Um guia para Byte Buddy

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.