Persistência sem complexidade (minha experiência com o MentaBean)

[quote=AbelBueno]
Mas não acho que com proxy dinâmico você consiga alterar o tipo de retorno de um método (eu acho).
Daí, imagino, que o seu getUserName e getBirthdate teria que continuar retornando seus tipos originais.
Se você fosse adicionar meta informação ao método, teria que obter de outra forma.
Pelo menos é isso que eu acho.[/quote]

Acho que não expliquei bem o que eu estava querendo mostrar.
A manipulação de bytecode não é para mudar o tipo de retorno, nem para fazer operações comuns de Reflection como descobrir atributos de uma classe.

Ela serve para criar um proxy dinâmico do objeto User, sobrescrevendo os métodos get. (pois a API de proxy padrão do Java só permite implementar interfaces, não sobrescrever classes!)
Vou colocar mais em detalhe o que aconteceria:

  • Ao executar a chamada .field([color=red]userAttributes.getUsername()[/color], DBTypes.STRING) , o método userAttributes.getUsername() será avaliado antes do .field(), ok?
  • A implementação do proxy , ao receber a chamada, vai calcular o nome do atributo (usando o nome do próprio método invocado) e guardar em algum lugar (pensando em um exemplo bem porco, seria uma variável static, mas claro que não estou sugerindo fazer assim… é só para ficar mais simples de entender, imagine que não existem situações de concorrência).
  • Em seguida o .field() é invocado, e pega o valor nessa variável temporária. O valor do parâmetro em si é ignorado.
  • E assim tudo se encaixa, a cada chamada encadeada de .field() e userAttributes.getXXX() os nomes dos campos vão sendo atribuídos.
  • O tipo retornado por getUsername() não importa, porque ele usou outra maneira para passar a informação que interessas; O método field() pode receber Object nesse parâmetro apenas para permitir que se passe qualquer coisa para ele.

Isso é o que eu inferi vendo o funcionamento do VRaptor (e suas dependências, muitas são libs para manipular bytecode e criar proxy de classes), se tiver algum desenvolvedor do framework por aqui pode corrigir se eu disser alguma besteira.


Mas o que eu quero é tirar TODOS esses atributos identificados por nome, e usar coisas mais amigáveis para refatoração. Inclusive porque refatoração é o grande ponto forte da configuração programática.

É disso mesmo que eu estava falando, mas de uma forma mais suave: no Hibernate você trabalha com objetos proxy o tempo todo, já nesse caso eles estariam em ação apenas durante a configuração.

Ora, vamos lá, você não quer ser como eles, quer? 8)

Neste ponto tenho que concordar contigo.

Na prática, não vejo tantos casos de atributos persistidos sem um get, mas não é papel do framework limitar isso.

Só ainda não entendi como um proxy atuaria ali, como você comentou no caso do EasyMock.
Tem uma idéia de como seria o código do método field() para isso?[/quote]

Seria uma abordagem parecida com a do Mock de Annotation que criei:

https://github.com/ataxexe/trugger/blob/master/src/main/java/org/atatec/trugger/util/mock/AnnotationMock.java

Resource resource = new AnnotationMock<Resource>(){{ map("name").to(annotation.name()); map(false).to(annotation.shareable()); }}.mock();

O método annotation.name() é de um proxy que vai colocar o método Resource#name como último método chamado para o AnnotationMock. Quando o método “to” for chamado, o método “name” já populou aquele valor e “to” saberá que “name” se refere ao último método chamado. (Não sei se ficou claro…acho que me embananei pra explicar.)

Analogamente, o método field já saberia que o método getSeiLaOQue foi chamado e usaria o nome da propriedade em questão para mapear o atributo.

No EasyMock, quando queremos simular comportamento de métodos void, usamos, em vez do “expect”, “expectLastCall”, já que não podemos alterar o retorno do método void. No caso do MentaBean isso não seria necessário (nem do meu mock de Annotations).

Entendi e é uma sacada legal mesmo. É o tipo de coisa que só dá para fazer com configuração programática. :slight_smile:

Mas ficou uma dúvida: o método userAttributes.getAge() retorna um int no compile time. Isso porque o userAttributes é do tipo User, certo? Como de repente esse cara vai me retornar um String no run-time? É aí que entra a magia negra da alteração de byte-code ou eu não entendi alguma coisa?

Na verdade não rs…
Mas pela explicação do gomesrod já tinha entendido.

Queria entender o que eles faziam com o retorno do método em si, mas realmente é jogado fora.
Era isso que tava dizendo sobre não consegui aproveitar o retorno assim.

Era minha dúvida…dá uma olhada na explicação do gomesrod

[quote=saoj][quote=Ataxexe]

[/quote]

Entendi e é uma sacada legal mesmo. É o tipo de coisa que só dá para fazer com configuração programática. :slight_smile:

Mas ficou uma dúvida: o método userAttributes.getAge() retorna um int no compile time. Isso porque o userAttributes é do tipo User, certo? Como de repente esse cara vai me retornar um String no run-time? É aí que entra a magia negra da alteração de byte-code ou eu não entendi alguma coisa?[/quote]

Só pra ilustrar, um trecho do método map:

[code]public <E> Mapper<E, T> map(final E value) {
return new Mapper<E, T>() {

  public org.atatec.trugger.util.mock.AnnotationMock&lt;T&gt; to(E expected) {
    mappings.put(lastCall, value);
    return org.atatec.trugger.util.mock.AnnotationMock.this;
  }
};

}

[/code]

Repare que eu nem me importo com o valor de expected, ele é usado aqui simplesmente por comodidade, assim a chamada ao método do proxy pode ficar na mesma linha da configuração.

Entendi ainda não. :slight_smile:

O próprio gomesrod não explicou isso:

Qual é essa outra maneira? :slight_smile:

O meu método é assim:

public int getAge() {
    return age;
}

O que eu faço para ele retornar Object no runtime?

Nunca trabalhei com alteração de bytecode e cglib, logo não tenho idéia.

[quote=saoj][quote=Ataxexe]

[code]public <E> Mapper<E, T> map(final E value) {
return new Mapper<E, T>() {

  public org.atatec.trugger.util.mock.AnnotationMock&lt;T&gt; to(E expected) {
    mappings.put(lastCall, value);
    return org.atatec.trugger.util.mock.AnnotationMock.this;
  }
};

}

[/code]
[/quote]

Entendi ainda não. :slight_smile:

O próprio gomesrod não explicou isso:

Qual é essa outra maneira? :slight_smile:

O meu método é assim:

public int getAge() {
    return age;
}

O que eu faço para ele retornar Object no runtime?

Nunca trabalhei com alteração de bytecode e cglib, logo não tenho idéia.

[/quote]

Entendi o que você quis dizer.

Nesse caso, o Java faz Autoboxing de int pra Integer, que pode ser tratado como um Object.

O fluxo é mais ou menos assim:

map(false).to(annotation.shareable())

  • Aqui a variável lasCall é preenchida pelo proxy (annotation aqui é um Proxy) com o método Resource#shareable (esta é a outra maneira :slight_smile: )

[code]private class AnnotationMockInterception implements Interception {

@Override
public Object intercept(InterceptionContext context) throws Throwable {
  Method method = context.method();
  String name = method.getName();
  if (!mocked) {
    lastCall = name;
  }
  return mappings.containsKey(name) ? mappings.get(name) : context.nullReturn();
}

}[/code]

map(false)[b].to/b

nesse ponto, a variável lasCall está preenchida, por causa disso, pode-se colocar o valor “false” no mapeamento. Repare que não usamos o “retorno” do método shareable, pois esse método só é chamado para que o proxy popule a variável lastCall. A propriedade shareable é um boolean, por causa disso, o Java faz a conversão para Boolean, assim ele pode ir como um Object para o método to sem problemas.

Era isso?

Acho que não, pois tem que retornar String, ou um Object com o toString() sobrescrito para te informar o nome do atributo.

Se fosse: Integer getAge() vc poderia retornar um Integer com o toString() hackeado.

Mas como é int getAge() como vc vai retornar um Object aí visto que esse método retorna um primitivo? Autoboxing aqui não influi porque ele só acontece depois do primitivo ser retornado e não antes.

No meu entendimento se um método retorna primitivo, ele tem que retornar primitivo, a não ser que houve algum byte-code alteration sinistro.

[quote=saoj][quote=Ataxexe]
Era isso?
[/quote]

Acho que não, pois tem que retornar String, ou um Object com o toString() sobrescrito para te informar o nome do atributo.

Se fosse: Integer getAge() vc poderia retornar um Integer com o toString() hackeado.

Mas como é int getAge() como vc vai retornar um Object aí visto que esse método retorna um primitivo? Autoboxing aqui não influi porque ele só acontece depois do primitivo ser retornado e não antes.[/quote]

Bom, vamos por partes:

A String que irá informar o nome do atributo será informada pelo proxy em cima da entidade. Quando a chamada ao getAge for feita, o proxy verá que foi o método getAge, logo, a propriedade será “age”, e colocará “age” em uma variável que o método “field” irá usar.

Como o getAge será chamado antes de field, no método field teremos como obter o valor “age”, que foi atribuído pelo proxy.

Não será necessário, pois não faremos nada com o retorno de getAge, ele é usado apenas para sinalizar ao proxy que deve popular a variável de propriedade com o valor “age”.

Um exemplo:

[code]public static Object foo(Object o) {
return 1;
}

public static void bar() {
foo(1);
foo(false);
}[/code]

Isso compila normalmente (dentro de uma classe, claro). O Autoboxing é feito antes da chamada ao foo e antes do retorno de foo.

De forma bem simplista, quando criar um proxy dinâmico, terá nele um método assim:

public Object novoMetodo(Method metodoOriginal, Object objetoOriginal ) {
        
        String nomeDoMetodo = metodoOriginal.getName();
        
        armazenaNomeParaUsarDepois(nomeDoMetodo);
                                
        return metodoOriginal.invoke(objetoOriginal);
        
    }

Este novoMetodo intercepta todas chamadas originais, getAge, getBirthday, etc.

Como recebe um objeto Method, você consegue meta informação dos métodos: nome, tipo de retorno, etc.

No exemplo, estou chamado armazenaNomeParaUsarDepois para trabalhar com o nome do método depois.
Ele executa o método original no retorno, mas no caso do mapeamento ele poderia simplesmente retornar null e ignorar o original.

Dei uma googlada e parece que o method interceptor do CGLIB sempre retorna Object. Então um método que no compile retorna int not runtime vai retornar Integer. Se a variável ou parâmetro que está recebendo ele for um int, autoboxing acontece, se for um Object, então viola: um método que antes retornava um primitivo passou a retornar um objeto.

EDIT: Mas mesmo assim ainda fico com a pulga atrás da orelha se esse esquema de proxy não vai fazer o unboxing ANTES de retornar o objeto, para garantir o contrato do método com o retorno primitivo. Um método que retorna primitivo de repente retornar Objeto é algo legal mas assustador também.

Não só ele como todos os outros também. Como a assinatura do proxy precisa ser a mais genérica possível, isso só acontecerá se retornarmos Object, inclusive para os métodos void, que “retornam” null no proxy.

Não só ele como todos os outros também. Como a assinatura do proxy precisa ser a mais genérica possível, isso só acontecerá se retornarmos Object, inclusive para os métodos void, que “retornam” null no proxy.[/quote]

Entendi. O interessante é que o objeto chega no cliente ao invés de ser convertido (unboxing por exemplo) no meio do caminho para que o cliente receba o retorno esperado no contrato (primitivo por exemplo) e não algo diferente. Acho que aí escolheram flexibilidade sobre segurança. Poderia haver algo entre o proxy e o cliente para garantir que o contrato do tipo fosse respeitado.

[quote=saoj]EDIT: Mas mesmo assim ainda fico com a pulga atrás da orelha se esse esquema de proxy não vai fazer o unboxing ANTES de retornar o objeto, para garantir o contrato do método com o retorno primitivo. Um método que retorna primitivo de repente retornar Objeto é algo legal mas assustador também.
[/quote]

Se você mandar algo que não possa ser convertido dá um ClassCastException, ou um NullPointerException caso mande null em tipos primitivos. É uma lambança que só, mas é culpa dos tipos primitivos :slight_smile:

Essas exceptions são meio que o recurso da JVM pra indicar isso, mas colocar algo no proxy seria interessante mesmo. Dá pra implementar mole mole, acho que vou fazer uns testes com isso depois.

[quote=Ataxexe][quote=saoj]EDIT: Mas mesmo assim ainda fico com a pulga atrás da orelha se esse esquema de proxy não vai fazer o unboxing ANTES de retornar o objeto, para garantir o contrato do método com o retorno primitivo. Um método que retorna primitivo de repente retornar Objeto é algo legal mas assustador também.
[/quote]

Se você mandar algo que não possa ser convertido dá um ClassCastException, ou um NullPointerException caso mande null em tipos primitivos. É uma lambança que só, mas é culpa dos tipos primitivos :slight_smile:

Essas exceptions são meio que o recurso da JVM pra indicar isso, mas colocar algo no proxy seria interessante mesmo. Dá pra implementar mole mole, acho que vou fazer uns testes com isso depois.[/quote]

Digo que a JVM não deveria permitir isso. Um proxy pode fazer o que quiser mas na hora de retornar o valor ele teria que honrar o retorno do método original. Para os primitivos bastaria dar um unboxing ou jogar uma exception caso não fosse um wrapper (Integer, Long, etc.). Entendo que ele tem que retornar um Objeto que é genérico, mas antes dele bater na variável ou no parâmetro que o espera a JVM “enforçaria” o tipo esperado no contrato.

O que vc está me falando é que a JVM não faz isso então eu posso esperar um int e receber um Integer. :shock:

Assim eu retornaria um Integer com o toString() alterado para me retornar o nome da propriedade.

Se chegasse pra mim um int como o contrato determina eu não poderia fazer nada…

E pelo que entendi a coisa é ainda pior:

Eu posso esperar um int e receber uma String. :shock:

Acho que o pessoal assume que se o cara for fazer byte-code manipulation então tudo é possível. Acho que faz sentido… graças a isso essa sacada aí é possível.

Resumindo… teremos que utilizar a orientação a aspectos para usarmos as vantagens da refatoração? :shock:

Só um detalhezinho que não está deixando ter a visão correta: O nome da propriedade não vem do retorno do método get no proxy, na verdade o retorno desse método é totalmente ignorado!

Seguindo com essa pseudo-implementação, que ficou muito boa:

[quote=AbelBueno] public Object novoMetodo(Method metodoOriginal, Object objetoOriginal ) { String nomeDoMetodo = metodoOriginal.getName(); armazenaNomeParaUsarDepois(nomeDoMetodo); return metodoOriginal.invoke(objetoOriginal); }
[/quote]

O método armazenaNomeParaUsarDepois(nomeDoMetodo) vai guardar esse nome em algum lugar (por exemplo, em uma variavel estática ou outros exemplos que vc vai ver no link abaixo). O retorno pode ser qualquer coisa, desde que não quebre na hora da conversão para o tipo esperado na assinatura do método original.

Aí seu método .field() ficaria mais ou menos assim:

public BeanConfig field(Object property, DBTypes type) {
       String lastCalledGetter = buscaNoMesmoLugarQueOMetodoAnteriorColocou();
       // O primeiro parametro é ignorado! A informação vem daquele lugar usado no método armazenaNomeParaUsarDepois() do proxy

       // ... faz o que mais tiver que fazer para configurar esse campo....
       // ...

       return this;
}

A conclusão é que isso:

config.field(userAttributes.getName(), DBTypes.STRING);

é apenas um atalho para isso:

userAttributes.getName(); // Guarda o nome do método
config.field(null, DBTypes.STRING); // usa o nome do método.

Agora para realmente acrescentar uma novidade: aqui tem um exemplo completo de implementação desse tipo de estratégia:

Ficou claro agora, obrigado gomesrod.

O valor não vem do retorno do método. Vem de outro lugar.

Quem tiver curiosidade para ver como isso ficou implementado no MentaBean pode dar uma olhada aqui:

http://mentabean.soliveirajr.com/mtw/Page/ProxyMapping/pt/mentabean-mapeamento-via-proxy

Utilizei o Javassist para fazer isso:

PropertiesProxy.java
BeanConfig.java

Fica interessante com proxy mesmo, mas alguém teria alguma ideia de como ficariam as nested properties? Pois isso aqui:

... userProxy.getAddress().getId();
irá estourar uma NPE…