Aplicando a JSR-311 e o Jersey nos serviços de leilão
Nesta seção mostraremos como usar o Jersey para implementar os serviços do processo de leilão. Devido às limitações de espaço, escolhemos apenas uma parte dos serviços, mas de forma que seja possível ilustrar com clareza as diferenças.
A primeira etapa necessária é a configuração do Jersey no projeto. A distribuição binária estável mais recente no momento da escrita deste artigo é a 0.7. Esta distribuição pode ser obtida no site do projeto.
Devemos mapear todos os prefixos de URIs dos nossos serviços para um Servlet do Jersey. O trecho abaixo mostra um web.xml configurado com este mapeamento. Na nossa aplicação, como todas as URIs são de serviços REST, mapeamos toda a aplicação (/*) para o Servlet do Jersey.
<?xml version="1.0" encoding="UTF-8"?> <web-app> <servlet> <servlet-name>Jersey Web Application</servlet-name> <servlet-class>com.sun.ws.rest.spi.container.servlet.ServletContainer</servlet-class> <init-param> <param-name>com.sun.ws.rest.config.feature.Redirect</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>com.sun.ws.rest.config.feature.ImplicitViewables</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey Web Application</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
Além da configuração do Servlet, precisamos adicionar algumas bibliotecas para utilizar o Jersey. O conjunto mínimo de bibliotecas que devem ser colocadas na aplicação inclui o jersey.jar, jsr311-api.jar e asm.jar. Estas bibliotecas e mais algumas dependências estão presentes no diretório /lib da distribuição binária do Jersey.
Se não estiver usando Java SE 6 ou Java EE 5, você também precisará adicionar o JAXB ao projeto. Utilizando um servidor de aplicações Java EE 5 e as bibliotecas presentes na distribuição binária do projeto, você tem a garantia de ter todas as dependências necessárias.
Desenvolvimento dos serviços
Mostraremos agora como pode ser feita a implementação de alguns serviços do processo de leilão com o uso do Jersey. Somente um subconjunto dos serviços será apresentado neste artigo, mas a implementação completa pode ser vista no código fonte.
Começaremos pelos serviços correspondentes ao prefixo /usuario e depois falaremos também sobre os serviços do prefixo /avaliacao. A Tabela 4 lista os serviços que apresentaremos neste artigo.
|
URI |
Método |
Formato |
Efeito |
|
/usuario |
POST |
Usuario |
Cadastra um usuário. |
|
/usuario/{id} |
GET |
Usuario |
Busca um usuário. |
|
PUT |
Usuario |
Atualiza um usuário. |
|
|
/usuario/{id}/avaliacoes |
GET |
Coleção de avaliações |
Busca as avaliações recebidas por um usuário. |
|
/usuario/{id}/itens |
GET |
Coleção de itens |
Busca os itens anunciados por um determinado usuário. |
|
POST |
Item |
Usuário coloca novo item à venda. |
|
|
/avaliacao/{id} |
GET |
Avaliação |
Busca uma determinada avaliação. |
|
/avaliacao/de/{id}/para/{id} |
POST |
Avaliação |
Realização da avaliação de um usuário sobre outro. |
A listagem abaixo apresenta a classe UsuarioResource. Esta é uma das classes Recurso da nossa aplicação e nela estão todos os serviços do prefixo /usuario. A classe foi anotada com @Path(”usuario”), o que faz a associação da mesma com o prefixo citado. Além disso, a classe possui as anotações @ConsumeMime e @ProduceMime, que neste caso declaram que os serviços da mesma são capazes de consumir e gerar conteúdo nos formatos text/xml e application/json.
@Path("usuario") @ConsumeMime( { "text/xml", "application/json" }) @ProduceMime( { "text/xml", "application/json" }) public class UsuarioResource { private ItemService itemService; private UsuarioService usuarioService; private AvaliacaoService avaliacaoService; public UsuarioResource() { this.itemService = ServiceFactory.getItemService(); this.usuarioService = ServiceFactory.getUsuarioService(); this.avaliacaoService = ServiceFactory.getAvaliacaoService(); } @GET @Path("{usuarioId}") public Response buscarUsuario(@PathParam("usuarioId") String usuarioId) { Usuario usuario = usuarioService.buscar(usuarioId); if(usuario == null){ return Response.status(HttpServletResponse.SC_NOT_FOUND).build(); } Response resposta = Response.ok(usuario).build(); return resposta; } @POST public Response cadastrarUsuario(Usuario usuario) { usuario = usuarioService.cadastrar(usuario); try { return Response.created(new URI(usuario.getCodUsuario())).build(); } catch (URISyntaxException e) { throw new RuntimeException(e); } } @PUT @Path("{usuarioId}") public Response atualizarUsuario(Usuario usuario) { usuarioService.atualizar(usuario); return Response.ok().build(); } @POST @Path("{usuarioId}/itens") public Response cadastrarItem(@Context UriInfo uriInfo, @PathParam("usuarioId") String usuarioId, Item item) throws URISyntaxException { Usuario usuario = new Usuario(usuarioId); item = itemService.cadastrar(item, usuario); URI uriItem = new URI(uriInfo.getBaseUri() + "item/" + item.getCodItem()); return Response.created(uriItem).build(); } @GET @Path("{usuarioId}/itens") public Response buscarItensDoUsuario(@PathParam("usuarioId") String usuarioId) { // Verifica se o usuário existe if (this.usuarioService.buscar(usuarioId) == null) { return Response.status(Status.NOT_FOUND).build(); } List itens = itemService.buscarPorVendedor(new Usuario(usuarioId)); return Response.ok(new ItensUsuario(itens)).build(); } @GET @Path("{usuarioId}/avaliacoes") public AvaliacoesUsuario buscarAvaliacoesDoUsuario(@PathParam("usuarioId")String usuarioId){ Usuario usuario = new Usuario(usuarioId); List avaliacoes = avaliacaoService.buscarPorUsuario(usuario); return new AvaliacoesUsuario(avaliacoes); } }
O primeiro serviço é o de busca de usuário. Este método foi anotado com @Path(”{usuarioId}”). O casamento da anotação sobre o método com a anotação sobre a classe especifica que este método responde a requisições para a URI /usuario/{usuarioId}. Como o método também foi anotado com @GET, sabemos que as solicitações HTTP GET para /usuario/{usuarioId} serão tratadas por este método. Importante reparar no uso da anotação @PathParam para injetar no parâmetro usuarioId o valor que veio na URI.
Na resposta a esta solicitação, retornamos o status HTTP 200 (OK) e os dados do usuário no corpo da resposta. Caso o usuário não tenha sido encontrado, retornamos status 404 (Not Found). A listagem abaixo mostra a classe Usuario, que é manipulada por alguns serviços na classe UsuarioResource. Por simplicidade mostramos apenas a declaração da classe com os atributos.
@XmlRootElement public class Usuario { private String codUsuario; private String nome; private String login; private String email; private Item[] items; private Oferta[] ofertas; private Avaliacao[] avaliacoes; }
O segundo serviço presente em UsuarioResource é o de cadastro de usuário. Como não colocamos nenhuma anotação @Path sobre este método, ele está associado à URI da classe (/usuario). O método foi anotado com @POST, então ele responde às solicitações POST na URI citada. O parâmetro contendo os dados do usuário não recebeu nenhuma anotação, o que significa que ele é obtido do corpo da solicitação. Como o método de cadastro de usuário não tem as anotações @ConsumeMime e @ProduceMime, ele herda as declarações feitas sobre a classe. Sendo assim, podemos cadastrar usuários usando text/xml ou application/json. Na resposta à criação do usuário nós enviamos o status HTTP 201 (Created), colocando no header Location a URI do novo usuário.
O método de atualização de usuário recebe solicitações PUT em /usuario/{usuarioId}. Os dados do usuário também são consumidos do corpo da solicitação, e as operações com sucesso resultam no envio do status HTTP 200.
No método de cadastrar itens, usamos a anotação @Path para associar o serviço à URI /usuario/{usuarioId}/itens. Usamos a anotação @PathParam para extrair da URI o ID do usuário envolvido. Usamos também a anotação @Context para injetar a classe UriInfo, que nos fornece informações sobre a URI de acesso aos serviços. No final, usamos a UriInfo para colocar no header Location o caminho absoluto de acesso ao item recém-criado. A listagem abaixo mostra a classe Item, manipulada neste serviço.
@XmlRootElement public class Item { private String codItem; private String nome; private String descricao; private BigDecimal valorInicial; private boolean novo; private boolean vendido; }
No método de buscar itens do usuário temos o primeiro serviço que manipula coleções. Este método trata de solicitações GET à URI /usuario/{usuarioId}/itens. Para retornar a lista de itens do usuário foi criada a classe ItensUsuario, que simplesmente contém a lista. A listagem a seguir apresenta a declaração desta classe.
@XmlRootElement public class ItensUsuario { private List item; public ItensUsuario() {} public ItensUsuario(List itens) { this.item = itens; } }
O método de buscar avaliações do usuário é estruturalmente semelhante ao de buscar itens. Foi criada a classe AvaliacoesUsuario para retornar a lista de avaliações. Como esta é muito semelhante à ItensUsuario, ela será omitida. Para implementar os serviços do prefixo /avaliacao foi criada a classe AvaliacaoResource. Esta classe pode ser vista na listagem a seguir. Temos a anotação @Path registrando a URI desejada e também as anotações @ConsumeMime e @ProduceMime declarando que manipulamos text/xml e application/json.
@Path("avaliacao") @ConsumeMime( { "text/xml", "application/json" }) @ProduceMime( { "text/xml", "application/json" }) public class AvaliacaoResource { private AvaliacaoService avaliacaoService; public AvaliacaoResource() { this.avaliacaoService = ServiceFactory.getAvaliacaoService(); } @GET @Path("{avaliacaoId}") public Response buscarAvaliacao(@PathParam("avaliacaoId") String avaliacaoId) { Avaliacao avaliacao = avaliacaoService.buscar(avaliacaoId); if(avaliacao == null){ return Response.status(HttpServletResponse.SC_NOT_FOUND).build(); } return Response.ok(avaliacao).build(); } @POST @Path("de/{avaliador}/para/{avaliado}") public Response avaliarUsuario(@Context UriInfo uriInfo, @PathParam("avaliador") String avaliador, @PathParam("avaliado") String avaliado, Avaliacao avaliacao) throws URISyntaxException { Usuario usuarioAvaliado = new Usuario(avaliado); avaliacao = avaliacaoService.cadastrar(avaliacao, usuarioAvaliado); URI uri = new URI(uriInfo.getBaseUri() + "avaliacao/" + avaliacao.getCodAvaliacao()); return Response.created(uri).build(); } }
O primeiro serviço desta classe é o de busca de avaliação, que é muito semelhante ao serviço de busca de usuário que vimos anteriormente. Este serviço ficou mapeado em /avaliacao/{avaliacaoId}, recebendo solicitações GET.
O serviço de avaliar usuário é mais interessante. Extraímos dois parâmetros da URI e consumimos um recurso do corpo da solicitação. A URI /avaliacao/de/{avaliador}/para/{avaliado} é um bom exemplo da liberdade que temos na definição das URIs. Podemos moldá-las para aumentar a clareza das operações. Isto facilita a aproximação dos serviços com o nosso domínio da aplicação.
October 21st, 2008 at 7:49 am
Bruno,
pelo que entendi a classe UsuarioService, por exemplo, seria o handler para operações relacionadas a usuários. Ela iria ao banco buscar as informações q o serviço precisa ou cadastrar algo quando necessário… é isso?
October 21st, 2008 at 9:07 am
Oi A2, a idéia é essa mesmo. As classes Service da aplicação são baseadas no pattern Service Layer proposto pelo Martin Fowler no livro Patterns of Enterprise Application Architecture.
A idéia é que a camada de serviço (Service Layer) contenha lógica de negócio, faça controle transacional e acesse a camada de persistência para o que for necessário.
November 25th, 2008 at 11:59 pm
Como que você constuma consumir RESTful Web Services? atraves da biblioteca HttpClient? ou utilizando talvez a Jersey Client API?
Obrigado.
November 26th, 2008 at 7:20 am
Oi Lucas, atualmente uso o HttpClient sim, mas eu gostaria de algo mais alto nível. Pretendo em breve ver o estado atual do client do Jersey e do RESTEasy e ver se eles já são usáveis. Quando comecei a usar o Jersey não tinha disponível nada razoável na parte client, então implementei com o HttpClient mesmo.