JPA/Hibernate – Object Proxying (nos bastidores)

4 0

This post is also available in: English

“Ter pessoas com dificuldades em partilhar conhecimento é talvez muito mais perigoso do que ter pessoas sem conhecimento nenhum”. Mário Júnior

Há imensas frameworks por aí; Java é uma terra de frameworks. Usar frameworks poupa-nos algum tempo, porque deixamos de ter de lidar com um certo conjunto de problemas, simplesmente porque alguém já lidou com eles para nos proteger. Mas… nunca te perguntaste “como é que a magia acontece?”.

Na minha humilde opinião, há decisões que as frameworks que deixas entrar na tua solução tiveram de tomar, e das quais devias estar consciente. Por isso, o meu conselho é: confia na framework, mas arranja algum tempo para perceber como a magia funciona, porque — mais uma vez — isso vai poupar-te ainda mais tempo.

Mas, mais do que poupar-te tempo, dá-te a possibilidade de fazeres contribuições para o projecto da framework, e isso é, sem dúvida, algo que para mim, enquanto contribuidor de open-source, merece ser mencionado. “Vamos deixar de agir como simples consumidores na cadeia produtor-consumidor“, vamos ser produtores e contribuir para o futuro das nossas amadas frameworks (todas elas seria impossível). Se não pudermos contribuir com código, então contribuamos com opiniões e ideias.

Desculpa lá o discurso, fui possuído por uma espécie de espírito revolucionário. Vamos directos ao assunto: JPA e Hibernate.

JPA é uma especificação/standard do JAVA EE dedicada à persistência de dados. Não vou estar a explicar toda aquela cena dos “persistence providers, persistence unit”. O meu foco é como o Hibernate usa object proxying e as implicações de como isso funciona, por isso é bom que já saibas o que são JPA e Hibernate.

Intro

Há uns anos atrás, estava convencido de que a única forma de fazer proxy a um objecto em tempo de execução era através da interface que ele implementa, ou seja: se A implementa B, para criar um objecto proxy de A, eu precisaria de um objecto C que implementasse B e envolvesse A. Estava convencido de que não conseguia criar um objecto proxy em tempo de execução se o objecto a que eu queria fazer proxy não tivesse interface.

Continuei a aprender coisas novas e podes ser tentado a acreditar que já sei demais, mas… sou apenas um aprendiz que se entusiasma a partilhar o que acabou de aprender. Mais tarde descobri que havia outra forma de fazer proxy a objectos em tempo de execução que eu desconhecia. Esta nova forma está relacionada com um princípio simples e muito importante em Java:

Podes adicionar novo código à tua aplicação Java durante o tempo de execução.

Isto é possível graças à instrumentação e aos class loaders.

A instrumentação como oportunidade de object proxying

Quando começamos a aprender Java, a primeira coisa que metemos na cabeça é que o programa começa a partir do método main. Bem, isto é parcialmente verdade, porque podes ter um método que será executado antes do método main, que é o premain. É neste método que é suposto transformares as classes antes da execução do main. Podes adicionar novos métodos à classe, adicionar novos atributos, remover os que existem, fazer o que quiseres, até devolver uma classe de substituição ou até definir novas classes.

O método premain é a forma como a JVM te permite fazer instrumentação directamente a partir da tua aplicação Java. Podes estar a pensar “como é que algo disto é útil?”. Podes usar isso para facilitar o profiling, por exemplo: podias transformar os métodos de algumas classes, adicionando código para registar o tempo que cada método demora a processar. Obviamente, já existem ferramentas que fazem isto por ti.

Trouxe o conceito de instrumentação porque ele nos dá uma oportunidade de fazer proxy a objectos de uma forma que eu, pessoalmente, defino como proxying baseado em herança.

O proxying baseado em herança assenta nas seguintes afirmações:

  1. O object proxying é um processo que só afecta métodos públicos
  2. Os métodos públicos estão disponíveis para as subclasses
  3. Uma subclasse que sobrescreve todos os métodos públicos da sua superclasse pode ser considerada uma classe proxy.

É tudo uma questão de orientação a objectos, sobretudo de herança, e isso irritou-me bastante, porque não conseguia acreditar que me estava a escapar um princípio tão básico.

Vamos clarificar o proxying baseado em herança:

Se a classe B estende a classe A, então toda a instância de objecto de B é um A. Se a classe B sobrescreve todos os métodos públicos de A, então B é uma classe de proxy.

Esta cena do proxying baseado em herança é algo realmente interessante, mas… o que é que isto tem a ver com JPA e Hibernate? Bem, o Hibernate usa object proxying e estou prestes a explicar porquê e quando:

Hibernate e object proxying

A primeira coisa que quero que saibas é que o Hibernate não é a única framework que depende de object proxying; a maioria faz o mesmo, por exemplo: CDI, Spring.

Quando trabalhamos com entidades relacionadas em JPA, cabe-nos decidir a estratégia que queremos usar para ir buscar (fetch) as entidades relacionadas, e há duas estratégias possíveis: LAZY e EAGER. Quando escolhemos LAZY, o Hibernate só vai buscar a(s) instância(s) da entidade relacionada se invocarmos o método getter. Por exemplo, para a seguinte classe de entidade:

@Entity

public class Deposit extends BaseEntity {

    private double amount;

    @ManyToOne(fetch=FetchType.LAZY,optional=false)

    private Account account;

    public Account getAccount(){
        return account;
    }

    public void setAccount(Account a){
        this.account
    }

    //other getters and setter may come here

}

O Hibernate vai criar uma classe proxy durante o tempo de execução e vai sobrescrever o método getAccount, para que possa ir à tua base de dados buscar o registo assim que invocares esse método; é assim que funciona o fetching LAZY. Não é magia, vês.

Há outra optimização interessante que o Hibernate faz quando lida com colecções. Vejamos outro exemplo de entidade:

public class Clustomer extends BaseEntity {

    @ManyToOne(fetch=FetchType.LAZY,mappedBy="customer")

    private List accounts;

    public List getAccounts(){
        return this.accounts;
    }

    public void setAccounts(List accs){
        this.accounts = accs;
    }

}

Neste caso, o Hibernate também vai fazer proxy ao método getAccounts para tornar possível o fetching LAZY, e vai também devolver uma implementação personalizada da interface List. Se pensavas que ele te estava a devolver um ArrayList, então lamento desiludir-te: na verdade está a devolver-te um objecto instância de PersistentList. Este PersistentList que o Hibernate te devolve é um data-proxy. Podes pensar que contém instâncias de objectos, mas quando o Hibernate o devolve, a probabilidade de estar vazio é de 100%. O Hibernate vai buscar os registos à medida que iteras e interages com ele, ou seja: se invocares o método getAccounts e depois não fizeres nada com o objecto List devolvido, nenhuma query será disparada, o que é realmente interessante.

Falar do PersistentList é um bónus, porque está fora do âmbito deste artigo, mas estou tentado a revelar coisas, por isso deixa-me só dizer:

Se a variável accounts fosse do tipo ArrayList em vez de List, não funcionaria, porque o Hibernate não faria proxy à classe ArrayList. O PersistentList implementa a interface List, não estende nem faz proxy ao ArrayList. Lembras-te de quando eu disse “Saber como a coisa funciona vai poupar-te algum tempo”? Agora talvez estejas tipo “Pois é”, porque se souberes disto, não vais passar horas a tentar descobrir porque é que usar ArrayList não funciona com o mapeamento JPA.

Pronto. Dei-te, meu caro leitor, uma visão geral básica de como e porquê o Hibernate usa proxying baseado em herança; vejamos agora as implicações disso.

Implicações do proxying baseado em herança no Hibernate

Estava sentado numa cadeira, em casa, a programar, como sempre, quando uma mensagem apareceu no meu Telegram. Era um amigo a partilhar um problema com o Hibernate: estava a ter uns StackOverflowException e não percebia a razão. De certa forma, motivou-me a escrever este artigo, e esta secção específica está relacionada com esse dia.

“Os objectos devolvidos pelo Hibernate não devem ser serializados em JSON”.

Esta é uma afirmação do Mário Júnior, um programador de 24 anos vindo de África sem crédito internacional; então porque deverias acreditar? Porque eu consigo convencer-te a isso: eles não foram feitos para ser serializados em JSON e vão provavelmente atingir-te com um StackOverflowException quando o tentares fazer. As probabilidades de teres essa excepção ao serializar objectos instância de entidades para JSON são de 99%, e eis porquê:

Imagina um modelo de base de dados com duas entidades: Order e OrderItem. A entidade Order tem um atributo de colecção chamado items (LAZY FETCH) que representa as instâncias de OrderItem relacionadas com ela; por outro lado, a entidade OrderItem tem um atributo order (LAZY FETCH) que representa a instância de Order à qual pertence.

Se correres a seguinte query:

Select item from OrderItem item

E depois tentares serializar os objectos resultantes para JSON, será lançada uma StackOverflowException, porque vais entrar num ciclo infinito. A serialização de objecto-para-JSON (com Jackson e Gson) baseia-se no princípio de que só os atributos públicos devem ser serializados e que, se um atributo privado tiver getter e setter, então também tem de ser serializado, entãããão:

Um objecto OrderItem tem um método chamado getOrder que será proxado pelo Hibernate para que possa ir à base de dados buscar o objecto Order. Assim que esse objecto é obtido, será imediatamente serializado, o que significa que o método getItems será invocado e uma List de instâncias de OrderItem será devolvida, que também tem de ser imediatamente serializada, e o ciclo nunca pára: é um ciclo infinito.

É por isto que eu digo: não serializes em JSON os beans de Entidade JPA.

Há uma coisa que podes ter deixado escapar quando eu estava a falar de instrumentação. Sei que te lembras do método premain, mas… sabes que não há método main em JAVA EE, o que significa que também não há premain. Então, como e quando é que as classes são transformadas?

Não há premain num container Java Web/EE

Para ser honesto, o Hibernate não depende do premain. Em Java, as classes são definidas pelos class loaders. Carregar (load) uma classe não é o mesmo que defini-la. Carregar uma classe é simplesmente ler o bytecode e analisá-lo (parsing) sem publicar a classe definida nesse bytecode, enquanto definir uma classe é publicar a classe representada numa sequência/stream de bytecode.

O Hibernate faz scan ao bytecode das classes — claro que usa algum parser de bytecode como o javassist — e, com base nos metadados devolvidos por uma ferramenta dessas, cria as classes de proxy e depois publica/define essas classes para que possam ficar disponíveis. Há muito mais a acontecer durante a geração das novas classes, mas isso está fora do âmbito deste artigo, que já está enooooorme.

Obrigado por me acompanhares. Gostava que deixasses algum comentário aqui em baixo. Lembras-te do que eu disse lá em cima sobre contribuição? Pois é, habitua-te a isso.

É sempre um prazer.

(Visited 1 times, 1 visits today)

Elisio Leonardo

Elisio Leonardo is an experienced Web Developer, Solutions Architect, Digital Marketing Expert, and content producer with a passion for technology, artificial intelligence, web development, and entertainment. With nearly 15 years of writing engaging content on technology and entertainment, particularly Comic Book Movies, Elisio has become a trusted source of information in the digital landscape.