Introdução ao Javassist
1. Visão geral
Neste artigo, examinaremos a bibliotecaJavasisst (Java Programming Assistant).
Simplificando, essa biblioteca simplifica o processo de manipulação do bytecode Java usando uma API de alto nível que a do JDK.
2. Dependência do Maven
Para adicionar a biblioteca Javassist ao nosso projeto, precisamos adicionarjavassist ao nosso pom:
org.javassist
javassist
${javaassist.version}
3.21.0-GA
3. O que é bytecode?
Em um nível muito alto, todas as classes Java são gravadas em formato de texto sem formatação e compiladas no bytecode - um conjunto de instruções que pode ser processado pela Java Virtual Machine. A JVM converte as instruções do bytecode em instruções de montagem no nível da máquina.
Digamos que temos uma classePoint:
public class Point {
private int x;
private int y;
public void move(int x, int y) {
this.x = x;
this.y = y;
}
// standard constructors/getters/setters
}
Após a compilação, o arquivoPoint.class contendo o bytecode será criado. Podemos ver o bytecode dessa classe executando o comandojavap:
javap -c Point.class
Isso imprimirá a seguinte saída:
public class com.example.javasisst.Point {
public com.example.javasisst.Point(int, int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iload_1
6: putfield #2 // Field x:I
9: aload_0
10: iload_2
11: putfield #3 // Field y:I
14: return
public void move(int, int);
Code:
0: aload_0
1: iload_1
2: putfield #2 // Field x:I
5: aload_0
6: iload_2
7: putfield #3 // Field y:I
10: return
}
Todas essas instruções são especificadas pela linguagem Java; a large number of them are available.
Vamos analisar as instruções de bytecode do métodomove():
-
A instruçãoaload_0 está carregando uma referência na pilha da variável local 0
-
iload_1 está carregando um valor interno da variável local 1
-
putfield está definindo um campox do nosso objeto. Todas as operações são analógicas para o campoy
-
A última instrução é umreturn
Cada linha de código Java é compilada no bytecode com instruções apropriadas. A biblioteca Javassist torna a manipulação desse bytecode relativamente fácil.
4. Gerando uma classe Java
A biblioteca Javassist pode ser usada para gerar novos arquivos de classe Java.
Digamos que desejamos gerar uma classeJavassistGeneratedClass que implemente uma interfacejava.lang.Cloneable. Queremos que a classe tenha um campoid deint tipo. OClassFile é usado para criar um novo arquivo de classe eFieldInfo é usado para adicionar um novo campo para uma classe:
ClassFile cf = new ClassFile(
false, "com.example.JavassistGeneratedClass", null);
cf.setInterfaces(new String[] {"java.lang.Cloneable"});
FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);
Depois de criarmos umJavassistGeneratedClass.class, podemos afirmar que ele realmente tem um campoid:
ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
assertEquals(fields[0].getName(), "id");
5. Carregando instruções de bytecode da classe
Se quisermos carregar instruções de bytecode de um método de classe já existente, podemos obter umCodeAttribute de um método específico da classe. Então, podemos obter aCodeIterator para iterar sobre todas as instruções de bytecode desse método.
Vamos carregar todas as instruções de bytecode do métodomove() da classePoint:
ClassPool cp = ClassPool.getDefault();
ClassFile cf = cp.get("com.example.javasisst.Point")
.getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();
List operations = new LinkedList<>();
while (ci.hasNext()) {
int index = ci.next();
int op = ci.byteAt(index);
operations.add(Mnemonic.OPCODE[op]);
}
assertEquals(operations,
Arrays.asList(
"aload_0",
"iload_1",
"putfield",
"aload_0",
"iload_2",
"putfield",
"return"));
Podemos ver todas as instruções de bytecode do métodomove() agregando bytecodes à lista de operações, conforme mostrado na afirmação acima.
6. Adicionando Campos ao Bytecode de Classe Existente
Digamos que queremos adicionar um campo do tipoint ao bytecode da classe existente. Podemos carregar essa classe usandoClassPolle adicionar um campo a ela:
ClassFile cf = ClassPool.getDefault()
.get("com.example.javasisst.Point").getClassFile();
FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);
Podemos usar reflexão para verificar se o campoid existe na classePoint:
ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
List fieldsList = Stream.of(fields)
.map(Field::getName)
.collect(Collectors.toList());
assertTrue(fieldsList.contains("id"));
7. Adicionando Construtor ao Bytecode de Classe
Podemos adicionar um construtor à classe existente mencionada em um dos exemplos anteriores usando um métodoaddInvokespecial().
E podemos adicionar um construtor sem parâmetros invocando um método<init> da classejava.lang.Object:
ClassFile cf = ClassPool.getDefault()
.get("com.example.javasisst.Point").getClassFile();
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);
MethodInfo minfo = new MethodInfo(
cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);
Podemos verificar a presença do construtor recém-criado iterando sobre o bytecode:
CodeIterator ci = code.toCodeAttribute().iterator();
List operations = new LinkedList<>();
while (ci.hasNext()) {
int index = ci.next();
int op = ci.byteAt(index);
operations.add(Mnemonic.OPCODE[op]);
}
assertEquals(operations,
Arrays.asList("aload_0", "invokespecial", "return"));
8. Conclusão
Neste artigo, apresentamos a biblioteca Javassist, com o objetivo de facilitar a manipulação de códigos de bytes.
Focamos nos principais recursos e geramos um arquivo de classe a partir do código Java; também fizemos alguma manipulação de bytecode de uma classe Java já criada.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.