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:
- O object proxying é um processo que só afecta métodos públicos
- Os métodos públicos estão disponíveis para as subclasses
- 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.








Comentários Recentes