quinta-feira, 30 de junho de 2011

RESTful Web Services com Jersey

Olá povo,

O padrão REST (Representational State Transfer) está cada vez mais se tornando um padrão para compartilhamento de informações nos serviços web. Criado em 2000 esse padrão utiliza o conceito de recursos que são identificados por um ids globais e utiliza os próprios métodos do protocolo HTTP para realizar operações sobre esses recursos em um servidor Web. Por exemplo, o método GET obtém um recurso ou uma lista deles, enquanto que o PUT insere, o POST atualiza e o DELETE remove. Outra característica interessante do REST é que podemos optar pelo tipo de retorno do serviço, ele pode ser: XML, JSON, ATOM, etc.

Para estabelecer um padrão para disponibilização de Web Services utilizando RESTful, foi criada a JSR-311, e sua implementação de referência é o Jersey, desenvolvido pela Sun (que saudade de você :) que utilizaremos nesse post.

Faça o download do Jersey (http://jersey.java.net/) e descompacte-o em algum lugar do seu computador (usei a versão 1.8). Para o nosso exemplo, além dele, você vai precisar do Tomcat (utilizei a versão 6.0), Eclipse for Java EE Developers ou o Eclipse comum com o plugin WTP (Web Tools Platform)

Inicie um novo Web/Dynamic Web Project no Eclipse chamado BlogWS_REST. Copie os JARs que estão na pasta lib do ZIP do Jersey para a pasta WebContent/WEB-INF/lib do seu projeto. Em seguida, adicione esses arquivos no Build Path do projeto. Clicando com o botão direito sobre eles com o botão direito, Build Path > Add to build path.

Agora, abra o arquivo Web Content/WEB-INF/web.xml e adicione as linhas abaixo dentro da tag <web-app>

<servlet>
<servlet-name>Jersey REST Service</servlet-name>
<servlet-class>
com.sun.jersey.spi.container.servlet.ServletContainer
</servlet-class>
<init-param>
<param-name>
com.sun.jersey.config.property.packages
</param-name>
<param-value>
ngvl.javaee.rest
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey REST Service</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>

Quem já fez alguma coisa com Java Web já deve saber que estamos declarando o Servlet que responderá pelas requisições HTTP. As tags servlet-class está declarando o Servlet do Jersey. Passamos como parâmetros de inicialização o (init-param) o nome do pacote da nossa aplicação que terá os web services, no nosso caso, ngvl.javaee.rest. Depois informamos que esse servlet iniciará junto com o servidor (load-on-startup). E por último, fazemos o mapeamento de URL que esse Servlet responderá. No nosso caso, dizemos que qualquer requisição que venha pra http://servidor/BlogWS_REST/rest/ será tratado pelo Servlet do Jersey.

Agora vamos fazer o primeiro Web Service Rest. Crie a classe HelloResource e deixe-a conforme abaixo:

package ngvl.javaee.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayHello() {
return "Hello Jersey";
}
}

A anotação @Path informa o caminho para acessar esse serviço. O @GET diz o método HTTP que estamos utilizando. Já o @Produces determina o tipo de retorno que é dado, neste caso, texto plano.

Execute a aplicação (botão direito sobre o projeto Run as > Run on server) e depois execute no browser http://localhost:8080/BlogWS_REST/rest/hello. Você verá o texto "Hello Jersey" no navegador :)

Muito lindo! Mas na vida real não trabalhamos com "Olá mundo" :) Assim como no post que falei sobre o AXIS e Web Services com SOAP, vou mostrar como trabalhar com objetos. Crie a classe Pessoa e deixe-a conforme abaixo.

package ngvl.javaee.rest;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Pessoa {
private int id;
private String nome;
@DefaultValue("")
private String outro; // atributo opcional

// Contrutor padrão é obrigatório
public Pessoa(){
}

public Pessoa(int id, String nome, String outro) {
this.id = id;
this.nome = nome;
this.outro = outro;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public String getOutro() {
return outro;
}
public void setOutro(String outro) {
this.outro = outro;
}
}


A única observação sobre essa classe é a notação @XmlRootElement para que ela possa ser retornada como JSON, XML, whatever... :) Outro detalhe é o campo opcional "outro", isso fará que ele não seja obrigatório sua informação
Agora vamos a implementação do Web Service :)
package ngvl.javaee.rest;

import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/pessoas")
public class PessoaResource {

static List<Pessoa> pessoas;
static {
pessoas = new ArrayList<Pessoa>();
pessoas.add(new Pessoa(1, "Nelson", "info1"));
pessoas.add(new Pessoa(2, "Glauber", null));
pessoas.add(new Pessoa(3, "Vasconcelos", null));
pessoas.add(new Pessoa(4, "Leal", "info2"));
}

@GET
@Produces({ MediaType.APPLICATION_JSON })
public List<Pessoa> obterPessoas() {
return pessoas;
}

@GET
@Path("{pessoa}")
@Produces(MediaType.APPLICATION_JSON)
public Pessoa obterPessoa(@PathParam("pessoa") int id) {
return obterPessoas().get(id);
}

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
public String insert(Pessoa p){
if (p != null){
pessoas.add(p);
return "Funcionou!";
} else {
return "Erro ao adicionar pessoa.";
}
}
}


Quase todas as notações nós discutimos aqui. O método obterPessoas retorna um JSON com todas as pessoas da lista. Para acessar esse método, basta colocar no browser http://localhost:8080/BlogWS_REST/rest/pessoas o retorno será um arquivo nesse formato:

{"pessoa":[{"id":"1","nome":"Nelson"},{"id":"2","nome":"Glauber"},{"id":"3","nome":"Vasconcelos"},{"id":"4","nome":"Leal"}]}

Já o segundo método obtém apenas uma pessoa. Para acessa-lo, use a URL http://localhost:8080/BlogWS_REST/rest/pessoas/1 . Utilizar esse "1" no final é determinado pela notação @Path("{pessoa}") onde pessoa será o id do recurso desejado. O retorno dessa chamada será o arquivo abaixo:

{"id":"2","nome":"Glauber"}

O último método insere um objeto Pessoa na lista. Note que ele utiliza a notação @Consumes que indica que um parâmetro no formato JSON será enviado para esse método. Ou seja, um JSON será enviado e convertido "automagicamente" para um objeto Pessoa.

Bem pessoal, é isso. Esse post foi baseado nessa página aqui. Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

44 comentários:

Rafael Di Bernardo disse...

Glauber,

O parâmetro do método obterPassoa(int) requer a anotação @PathParam("pessoa"), caso contrário retornará um erro de tipo.

Segue o método já com a anotação:

@GET
@Path("{pessoa}")
@Produces( { MediaType.APPLICATION_JSON })
public Pessoa obterPessoa(@PathParam("pessoa") int id) {
return obterPessoas().get(id);
}

Após a modificação os valores são retornados corretamente.

Nelson Glauber de Vasconcelos Leal disse...

Valeu Rafael!

Você tá completamente certo.
Depois de ler seu comentário, ajustei, testei e está ok.

4br4ç05,
nglauber

Mychelle disse...

Olá, tenho uma dúvida, onde vc declarou a url http://localhost:8080/BlogWS_REST/rest/hello ?

Nelson Glauber de Vasconcelos Leal disse...

Oi Mychelle,

localhost quer dize que é a sua máquina local. 8080 a porta padrão do Tomcat. BlogWS_REST é o nome da pasta do Tomcat. /rest é definido no arquivo de configuração do jersey na tag url-pattern. e o /hello é definido na anotação @Path("/hello").

4br4ç05,
nglauber

Mychelle disse...

Obrigado, Nelson...

Paulo Vicente disse...

Nelson, obrigado e parabéns. O nivel de detalhamento das suas explicações facilita sobremaneira o entendimento de todo o artigo. Excelente.

Jhonathan Brandão disse...

Nelson, como faço um método POST consumir JSON? Não consegui achar isso de forma clara.

Nelson Glauber de Vasconcelos Leal disse...

Oi Jhonathan,

Basta colocar na assinatura do método @POST ao invés de @GET. E no cliente fazer um request via post. No exemplo que mostrei com Android, é só chamar o método setRequestMethod("POST") no objeto HttpURLConnection.

4br4ç05,
nglauber

Everton Vieira disse...

Grande Mestre! Existe algum motivo para o nome do Objeto ficar minusculo ({"pessoa":... e não {"Pessoa":... ) no JSon?

Júnior disse...

COmo posso passar 2 parametros pra o metodo de lerUmaPessoa por exemplo?

String result = getRESTFileContent("http://10.0.2.2:8080/BlogWS_REST/rest/pessoas/1parametro e 2parametro");

Nelson Glauber de Vasconcelos Leal disse...

Oi Junior,

Basta você colocar na assinatura do método a anotação @QueryParam.

Ex.:
public Pessoa obterPessoa(@PathParam("pessoa") int id, @QueryParam("foo") String a) {
}

E na hora de chamar:
http://localhost:8080/ExemploRESTServer/rest/pessoas/1?foo=Glauber

4br4ç05,
nglauber

Iomar Santos disse...

Testei mas sempre da erro de conexao recusada:

Nelson Glauber de Vasconcelos Leal disse...

Iomar provavelmente você deve estar usando o IP errado. Você não deve usar nem localhost nem 127.0.0.1, pois, para o emulador esses endereços representam ele próprio. Use 10.0.2.2 para que o emulador se comunique com a máquina local.

4br4ç05,
nglauber

Iomar Santos disse...

Apos fazer a alteração funcionou,
no me caso ficou assim:

Chamada
http://10.0.2.2:8080/ExemploRESTServer/rest/pessoas/ cliente

http://localhost:8080/ExemploRESTServer/server

obrigado Glauber, valeu pela ajuda!!

Anônimo disse...

Boa tade Nelson, ótimo artigo. Só fiquei com uma dúvida: Como fica a Chamada para o método de inserção?

Nelson Glauber de Vasconcelos Leal disse...

Oi Anônimo,

A resposta tá no outro post:
http://nglauber.blogspot.com/2011/06/android-restful-json.html

4br4ç05,
nglauber

Mychelle disse...

Olá Glauber, estou com uma dúvida em relação a pergunta do Junior, ele perguntou como enviar dois parâmetros para o web service, eu entendi como o método fica no web service, mas Como ficaria a chamada no android, ficaria assim?



String result = getRESTFileContent("http://10.0.2.2:8080/BlogWS_REST/rest/pessoas/parametro1?parametro2");

Mychelle disse...

Olá Glauber, já consegui fazer, mesmo assim obrigada. Boa noite.

Nelson Glauber de Vasconcelos Leal disse...

Oi Mychelle,

Que bom que conseguiu. Qualqeur coisa me fala.

4br4ç05,
nglauber

RH disse...

Glauber como ficaria o meu metodo inserirPessoa caso eu adicione 2 campos editext, campoCod e campoNome e inserir atraves dos valores digitados pelo usuario. Obrigado e parabens!

Nelson Glauber de Vasconcelos Leal disse...

Oi RH,

Você viu a parte editada do post? Eu mostro exatamente isso...

4br4ç05,
nglauber

GiGi disse...

profêêê... olha eu aqui de novo... hihihi... prometo que da proxima dou um tempo mais LONGO hihihi... veja bem.. como faço para consumir os dados de um webservice .NET no android... SEM TER NENHUMA CLASSE BÁSICA... :s preciso criar métodos ou so chamando os métodos da classe de comunicação já consigo esses dados? sem implementar nada de sql em android...?? :s HELP ME ME... mais uma vez novamente :) pleaseee :) vlw

Nelson Glauber de Vasconcelos Leal disse...

Oi Gisele,

Não entendi sua dúvida. Pra acessa um webservice .net depende do que ele está retornando. Se for REST, segue a mesma linha desse post, se for SOAP, você pode usar esse post aqui http://nglauber.blogspot.com.br/2011/02/web-services-no-android.html.

Qualquer dúvida, manda email.

4br4ç05,
nglauber

Diego Sousa disse...

Olá Glauber,

É possível consumir dois atributos de um Json, dessa forma abaixo? Mais o detalhe é que eu não queria que o login e a senha fosse passado na url, como o @PathParam obriga.

@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public User login(String login, String password) {
return facade.validateLogin(login, password);
}

Agradeço muito a ajuda, estou já a duas madrugadas procurando uma solução. Abraço

Nelson Glauber de Vasconcelos Leal disse...

Oi Diego,

Nesse outro artigo (http://nglauber.blogspot.com.br/2011/06/android-restful-json.html), mostro como utilizar o webservice acima dando um POST no webservice. Consequentemente não passaria o login e senha na URL.
4br4ç05,
nglauber

Diego disse...

Obrigado Nelson,

Consegui resolver me baseando no seu exemplo. Apenas utilizei JSONObject (Não tinha conhecimento da existência dessa classe) para recuperar as chaves passadas no meu Json e deu tudo certo!

Agora achei estranho o Package javax.ws.rs não ter nenhum método que receba de um Json um valor e salve esse valor em um atributo primitivo. Só vi formas de converter para um objeto.

Larissa Leite disse...

Ola, vc teria algum tutorial sobre como consumir um WS REST, como o do Twitter?

Nelson Glauber de Vasconcelos Leal disse...

Oi Larissa,

Dá uma olhada nesse post (http://nglauber.blogspot.com.br/2011/06/android-restful-json.html). Depois é só ajustar pra ler o JSON do Twitter.

4br4ç05,
nglauber

PedroGF disse...

Muito bom o tutorial Glauber, ficou simples e bem explicativo sobre como fazer um web service!

Anônimo disse...

Bom dia Nelson, primeiro quero agradecer pelo seu tutorial RESTful Web Services com Jersey, segui ele e funcionou perfeito.. queria tirar uma duvida, queria saber se tem como eu rodar o servidor em uma maquina onde não tem eclipse instalado, tipo criar um executável para não precisar instalar e ficar abrindo o eclipse, tentei criar o .jar, mas como não tem método principal não consegui fazer... Obrigado pela ajuda..
Cleber Alves
clebermcosmeticos@hotmail.com

Nelson Glauber de Vasconcelos Leal disse...

Oi Anônimo,

É possível sim. Basta exportar seu projeto como um WAR (Web ARchive) ao invés de JAR e fazer o deploy no seu servidor.

4br4ç05,
nglauber

Cleber disse...

Obrigado pela dica, usei o tomcat, e esta rodando beleza, agora estou tentando tratar a busca quando o resultado é null ao importar..

Andre Lima disse...

Muito bom. Dentre todos exemplos que testei na Web, realmente este foi o melhor. Parabéns.

mario jorge disse...

Minha estrutura ficou desta sabe me informar o porque?

{"pessoa":[{"id":"1","info":"info1","nome":"Nelson"},{"id":"2","info":"info2","nome":"Glauber"},{"id":"3","info":"info3","nome":"Vasconcelos"},{"id":"4","info":"info4","nome":"Leal"}]}

Nelson Glauber disse...

Oi mario,

É exatamente esse o formato esperado conforme eu descrevi no post.

4br4ç05,
nglauber

Raphael Mendes disse...

Olá Glauber, estou uma dúvida, você poderia me ajudar?
A dúvida é a seguinte: Quando um serviço REST precisa de vários método @GET, por exemplo:
@GET
@Path("{pessoa}") @Produces(MediaType.APPLICATION_JSON)
public Pessoa obterId(@PathParam("pessoa") int id) {
return obterPessoas().get(id);
}



@GET
@Path("{nome}") @Produces(MediaType.APPLICATION_JSON) public Pessoa obterNome(@PathParam("nome") String nome) {
return obterPessoas().get(nome);
}

No primeiro método a url de identificação seria: http://localhost:8080/BlogWS_REST/rest/pessoas/1
No segundo metodo a url de identificação seria:
http://localhost:8080/BlogWS_REST/rest/pessoas/maria
Mas quando executo o projeto, sempre o primeiro método é chamado independente do tipo de parâmetro passado na url. Sendo assim como ficaria o caminho da url e como seria a chamada no browser? Desde já agradeço.

Nelson Glauber disse...

Oi Raphael,

Quando queremos acessar um recurso específico (no nosso exemplo, uma pessoa) passamos o ID daquela pessoa na URL. Mas nesse caso que você tá falando, "maria" não é o ID da pessoa, e sim você está querendo fazer a busca por uma pessoa cujo nome é "maria". Então você teria uma URL do tipo http://seuservidor/pessoas?nome=maria.

4br4ç05,
nglauber

Mychelle disse...

Boa noite Glauber.
Gostaria que você tirasse uma dúvida se for possível. Eu posso ter vários @Get em um único recurso, por exemplo. Um método @Get que busca uma pessoa pelo nome e um outro método @Get que busca uma pessoa pelo cpf? Eu criei em meu projeto dois métodos, que são os seguintes:
@Get
@Path("{nome}") @Produces(MediaType.APPLICATION_JSON)
public List getClienteByNome(@PathParam("nome") String nome) throws ClassNotFoundException{
ClienteBean cliente = new ClienteBean();
cliente.setCliNome(nome);
ClienteDao dao = new ClienteDao();
list = dao.searchByName(cliente);
return list;
}

O segundo método que criei foi:
@Get
@Path("{cpf}") @Produces(MediaType.APPLICATION_JSON)
public ClienteBean getClienteByCpf(@PathParam("cpf") String cpf) throws ClassNotFoundException{
ClienteBean cliente = new ClienteBean();
cliente.setCliCpf(cpf);
ClienteDao dao = new ClienteDao();
dao.searchByCpf(cliente);
return cliente;
}

Mas ocorre a seguinte exception quando executo o projeto pelo browser:
HTTP Status 500 - Internal Server Error

javax.servlet.ServletException: Servlet.init() for servlet helloWorld.ApplicationConfig threw exception

java.lang.IllegalStateException: The resource configuration is not modifiable in this context.

Uso a seguinte url para acessar os recursos:
para nome: http://localhost:8080/HelloWorldApplication/webresources/pessoas?nome=joana

para cpf:
http://localhost:8080/HelloWorldApplication/webresources/pessoas?cpf=12345

Quando eu retiro um dos métodos @Get, ficando apenas um método @Get, funciona legal o projeto. Desde já agradeço a atenção.

Raphael Mendes disse...

Obrigado, a pesquisa funcionou!

Nelson Glauber disse...

Oi Mychelle,

Vê a resposta que eu dei pro Raphael.

4br4ç05,
nglauber

Unknown disse...

Nelson, como consumir esse webservice pelo Android?

Nelson Glauber disse...

Oi Unknown,

O post é velhinho mas ainda serve...
http://www.nglauber.com.br/2011/06/android-restful-json.html

4br4ç05,
nglauber

Andre disse...

Oi Glauber, boa tarde

estou com uma duvida! tenho um web service com dois parâmetros !! segue o codigo abaixo :

@Consumes({MediaType.APPLICATION_JSON, "text/plain"})
@Produces("text/plain")
@Path("/votacaoAta")
public String votarAta(TbDetalhesvotacaodeatape votAta, @QueryParam("url") String url) {
try {
FacesUtil.url = url;
hbutil = new HibernateUtil();
DetalhesVotacaoAtaPeRN rnAtaRN = new DetalhesVotacaoAtaPeRN(hbutil);
rnAtaRN.alterar(votAta);
hbutil.getSessionFactory().close();
return "S";
} catch (Exception e) {
return "N";
}
}

só que o objeto vem preenchido mais a url não!!! o que será? na minha ideia quero que url seja via url e o objeto um stream fora da url como posso fazer?

grato

Nelson Glauber disse...

OI André,

Esse post é bem velhinho (5 anos :) então faz tempo que não uso essa API, mas...
Você está passando a URL no formato:
http://seuservidor/votacaoAta?url="url_desejada"

Será que você não precisa dar um "escape" nos caracteres especiais?

4br4ç05,
nglauber