Páginas

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:

  1. 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.

    ResponderExcluir
  2. Valeu Rafael!

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

    4br4ç05,
    nglauber

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

    ResponderExcluir
  4. 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

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

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

    ResponderExcluir
  7. 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

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

    ResponderExcluir
  9. 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");

    ResponderExcluir
  10. 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

    ResponderExcluir
  11. Testei mas sempre da erro de conexao recusada:

    ResponderExcluir
  12. 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

    ResponderExcluir
  13. 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!!

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

    ResponderExcluir
  15. Oi Anônimo,

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

    4br4ç05,
    nglauber

    ResponderExcluir
  16. 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");

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

    ResponderExcluir
  18. Oi Mychelle,

    Que bom que conseguiu. Qualqeur coisa me fala.

    4br4ç05,
    nglauber

    ResponderExcluir
  19. 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!

    ResponderExcluir
  20. Oi RH,

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

    4br4ç05,
    nglauber

    ResponderExcluir
  21. 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

    ResponderExcluir
  22. 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

    ResponderExcluir
  23. 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

    ResponderExcluir
  24. 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

    ResponderExcluir
  25. 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.

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

    ResponderExcluir
  27. 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

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

    ResponderExcluir
  29. 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

    ResponderExcluir
  30. 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

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

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

    ResponderExcluir
  33. 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"}]}

    ResponderExcluir
  34. Oi mario,

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

    4br4ç05,
    nglauber

    ResponderExcluir
  35. 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.

    ResponderExcluir
  36. 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

    ResponderExcluir
  37. 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.

    ResponderExcluir
  38. Oi Mychelle,

    Vê a resposta que eu dei pro Raphael.

    4br4ç05,
    nglauber

    ResponderExcluir
  39. Nelson, como consumir esse webservice pelo Android?

    ResponderExcluir
  40. Oi Unknown,

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

    4br4ç05,
    nglauber

    ResponderExcluir
  41. 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

    ResponderExcluir
  42. 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

    ResponderExcluir