Um guia para Byte Buddy
*1. Visão geral *
Simplificando, ByteBuddy é uma biblioteca para gerar classes Java dinamicamente em tempo de execução.
Neste artigo, vamos usar a estrutura para manipular classes existentes, criar novas classes sob demanda e até interceptar chamadas de método.
===* 2. Dependências *
Vamos primeiro adicionar a dependência ao nosso projeto. Para projetos baseados em Maven, precisamos adicionar essa dependência ao nosso pom.xml:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.7.1</version>
</dependency>
Para um projeto baseado em Gradle, precisamos adicionar o mesmo artefato ao nosso arquivo build.gradle:
compile net.bytebuddy:byte-buddy:1.7.1
A versão mais recente pode ser encontrada em Maven Central
===* 3. Criando uma classe Java em tempo de execução *
Vamos começar criando uma classe dinâmica subclassificando uma classe existente. Vamos dar uma olhada no projeto clássico "Olá Mundo".
Neste exemplo, criamos um tipo (Class) que é uma subclasse de Object.class e substituímos o método _toString () _:
DynamicType.Unloaded unloadedType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.isToString())
.intercept(FixedValue.value("Hello World ByteBuddy!"))
.make();
O que acabamos de fazer foi criar uma instância de ByteBuddy. Em seguida, usamos a API subclass () para estender Object.class e selecionamos o toString () _ da super classe (_Object.class) usando ElementMatchers.
Finalmente, com o método _intercept () _, fornecemos nossa implementação de _toString () _ e retornamos um valor fixo.
O método _make () _ aciona a geração da nova classe.
Nesse ponto, nossa classe já está criada, mas ainda não foi carregada na JVM. É representado por uma instância de DynamicType.Unloaded, que é uma forma binária do tipo gerado.
Portanto, precisamos carregar a classe gerada na JVM antes de podermos usá-la:
Class<?> dynamicType = unloadedType.load(getClass()
.getClassLoader())
.getLoaded();
Agora, podemos instanciar o dynamicType e chamar o método _toString () _ nele:
assertEquals(
dynamicType.newInstance().toString(), "Hello World ByteBuddy!");
Observe que a chamada dynamicType.toString () _ não funcionará, pois apenas invocará a implementação _toString () _ de _ByteBuddy.class.
O newInstance () _ é um método de reflexão Java que cria uma nova instância do tipo representado por este objeto _ByteBuddy; de maneira semelhante ao uso da palavra-chave new com um construtor no-arg.
Até o momento, só conseguimos substituir um método na superclasse do nosso tipo dinâmico e retornar nosso próprio valor fixo. Nas próximas seções, veremos como definir nosso método com lógica personalizada.
===* 4. Delegação de método e lógica personalizada *
No exemplo anterior, retornamos um valor fixo do método _toString () _.
Na realidade, os aplicativos exigem uma lógica mais complexa que essa. Uma maneira eficaz de facilitar e provisionar lógica personalizada para tipos dinâmicos é a delegação de chamadas de método.
Vamos criar um tipo dinâmico que subclasses Foo.class que possui o método _sayHelloFoo () _:
public String sayHelloFoo() {
return "Hello in Foo!";
}
Além disso, vamos criar outra classe Bar com um _sayHelloBar () _ estático da mesma assinatura e tipo de retorno que _sayHelloFoo () _:
public static String sayHelloBar() {
return "Holla in Bar!";
}
Agora, vamos delegar todas as chamadas de sayHelloFoo () _ para _sayHelloBar () _ usando o DSL de _ByteBuddy. Isso nos permite fornecer lógica personalizada, escrita em Java puro, para nossa classe recém-criada em tempo de execução:
String r = new ByteBuddy()
.subclass(Foo.class)
.method(named("sayHelloFoo")
.and(isDeclaredBy(Foo.class)
.and(returns(String.class))))
.intercept(MethodDelegation.to(Bar.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance()
.sayHelloFoo();
assertEquals(r, Bar.sayHelloBar());
Invocar o _sayHelloFoo () _ invocará o _sayHelloBar () _ de acordo.
*Como _ByteBuddy_ sabe qual método em _Bar.class_ deve ser chamado?* Ele escolhe um método correspondente de acordo com a assinatura do método, tipo de retorno, nome do método e anotações.
Os métodos _sayHelloFoo () _ e _sayHelloBar () _ não têm o mesmo nome, mas têm a mesma assinatura de método e tipo de retorno.
Se houver mais de um método invocável em Bar.class com assinatura correspondente e tipo de retorno, podemos usar a anotação _ @ BindingPriority_ para resolver a ambiguidade.
_ @ BindingPriority_ usa um argumento inteiro - quanto maior o valor inteiro, maior a prioridade de chamar a implementação específica. Portanto, _sayHelloBar () _ será preferível a _sayBar () _ no trecho de código abaixo:
@BindingPriority(3)
public static String sayHelloBar() {
return "Holla in Bar!";
}
@BindingPriority(2)
public static String sayBar() {
return "bar";
}
*5. Definição de Método e Campo *
Conseguimos substituir os métodos declarados na superclasse dos nossos tipos dinâmicos. Vamos além, adicionando um novo método (e um campo) à nossa classe.
Usaremos a reflexão Java para chamar o método criado dinamicamente:
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.name("MyClassName")
.defineMethod("custom", String.class, Modifier.PUBLIC)
.intercept(MethodDelegation.to(Bar.class))
.defineField("x", String.class, Modifier.PUBLIC)
.make()
.load(
getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
Method m = type.getDeclaredMethod("custom", null);
assertEquals(m.invoke(type.newInstance()), Bar.sayHelloBar());
assertNotNull(type.getDeclaredField("x"));
Criamos uma classe com o nome MyClassName que é uma subclasse de Object.class. Em seguida, definimos um método custom, _ que retorna uma _String e possui um modificador de acesso public.
Assim como fizemos nos exemplos anteriores, implementamos nosso método interceptando chamadas e delegando-as para Bar.class que criamos anteriormente neste tutorial.
===* 6. Redefinindo uma classe existente *
Embora tenhamos trabalhado com classes criadas dinamicamente, também podemos trabalhar com classes já carregadas. Isso pode ser feito redefinindo (ou reformulando) as classes existentes e usando ByteBuddyAgent para recarregá-las na JVM.
Primeiro, vamos adicionar ByteBuddyAgent ao nosso pom.xml:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.7.1</version>
</dependency>
A versão mais recente pode ser found here
Agora, vamos redefinir o método sayHelloFoo () _ que criamos em _Foo.class anteriormente:
ByteBuddyAgent.install();
new ByteBuddy()
.redefine(Foo.class)
.method(named("sayHelloFoo"))
.intercept(FixedValue.value("Hello Foo Redefined"))
.make()
.load(
Foo.class.getClassLoader(),
ClassReloadingStrategy.fromInstalledAgent());
Foo f = new Foo();
assertEquals(f.sayHelloFoo(), "Hello Foo Redefined");
===* 7. Conclusão*
Neste guia elaborado, examinamos extensivamente os recursos da biblioteca ByteBuddy e como usá-lo para a criação eficiente de classes dinâmicas.
Seu documentation oferece uma explicação detalhada do funcionamento interno e de outros aspectos da biblioteca.
E, como sempre, os trechos de código completos deste tutorial podem ser encontrados over no Github.