Android

14 dez, 2015

Criando um Proxy em Runtime no Java (e no Android)

Publicidade

Para a maioria das pessoas, Proxy é uma coisa que o pessoal de TI inventa para não deixar acessar o Facebook e companhia no trabalho. Ou seja, é um sistema intermediário que o browser acessa antes de chegar na selva da internet. Algo assim:

network_proxy-300x135
Poxy de rede

Em desenvolvimento também há o conceito de Proxy e até que é semelhante: em Java, Proxy é uma classe que serve para intermediar uma implementação de interface. Vejamos os detalhes com calma.

Pense, por exemplo, em um framework de requisições REST. Sei lá, um framework que permita que você defina contratos de métodos em uma interface e que possa colocar metadados nestes métodos para indicar os detalhes da chamada. Vamos ver… Talvez usar anotações como @GET, @POST, @PUT, @DELETE, @Body, @Headers, @Path, @Query etc.

Parece legal, né? Até passo um exemplo do que queremos que seja nossa interface:

public interface GitHubService {
  @GET("/users/{user}/repos")
  List<Repo> listRepos(@Path("user") String user);
}

Olha que legal: só de olhar para as anotações (metadados) sei dizer que este método deve executar um HTTP GET para um servidor passar um path dinâmico e que vai me retornar uma lista de objetos já parseados. Top de linha!

Qualquer semelhança com o Retrofit é mera coincidência (a versão 2.0.0 vai exigir que o retorno seja sempre Call<T>)! Só que não! Mas como ele funciona por debaixo dos panos? Como ele consegue “implementar” esta interface e fazer a chamada em tempo de execução?

Este é um ótimo exemplo de utilização de Proxy! A interface é uma forma de estabelecermos um contrato de execução e funciona como um Proxy para a implementação que o Retrofit cria em tempo de execução (runtime).

Não se assustem com o diagrama a seguir, mas tenho que mostrar o processo completo:

Diagrama Proxy em Java
Diagrama Proxy em Java

Eeeeeeeeitcha (disse um colega de trabalho quando apresentei o diagrama)… Mas vamos por partes.

Da esquerda para a direita o que acontece é:

  • O usuário tem uma referência para um objeto do tipo GitHubService. Ela é o Proxy no diagrama;
  • Ele executa o método listRepos(“usuário”) no Proxy;
  • O proxy internamente possui um InvocationHandler (Tratador de invocações de método) que é acionado quando o usuário chama qualquer método no proxy (inclusiveequals(Object o) e hashCode()).
  • O InvocationHandler chama seu único método abaixo que retorna um Object que deve ser igual ao retorno do método declarado no da interface, caso contrário teremos uma exceção. Além disso, ele recebe a instância do Proxy (que pode ser útil para compararmos se é do tipo de uma interface específica), o método que foi invocado e um array de Object que são os parâmetros passados na invocação do método. No nosso exemplo, o Proxy implementa GitHubService, o método é listRepos (o que poderíamos verificar com method.getName()) e args seria igual a new Object[]{ “usuário” }:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  • O invocationHandler faz seu trabalho e retorna o que a interface espera. No nosso exemplo seria uma List<Repo>.

Talvez a sensação de “EEEEEEEEEEEeeeeeeeeitcha” tenha melhorado agora. Proxies são úteis para implementações que precisam de contexto do tempo de execução. Porém, tenham muita cautela nas implementações, pois dentro do InvocationHandler para fazer qualquer coisa que não seja muito simples, será necessário usar bastante reflexão. Você irá precisar saber os tipos dos parâmetros passados, recuperar anotações que eles possam ter ou que o próprio método possa ter entre outras configurações gerais.

Outro ponto de atenção é que debuggar isto pode ser um pouco estranho no começo. Vejamos o porquê. Para criar um Proxy usamos o seguinte método:

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler) throws IllegalArgumentException

Isso retorna Object, que é uma implementação de todas as interfaces passadas no array interfaces. Ou seja, nosso Proxy dinâmico é de vários tipos ao mesmo tempo. Além disso, ele pode ter casos de erro que não acontecem normalmente em chamadas diretas de objetos. Imagine chamar um toString() que retorna um Integer! Não tem como acontecer, apenas em Proxies.

Por isso é sempre bom conhecer o que a plataforma nos fornece e também como os frameworks que usamos no dia a dia funcionam por baixo dos panos, porém sempre exercitem cautela ao decidir uma arquitetura de solução. Quase sempre estaremos adicionando complexidade com grande possibilidade de erros.

É isso aí, pessoal! Lembrem de ler a documentação da classe Proxy. Se ficar alguma dúvida ou sugestão, não hesitem em comentar aqui.