Why am I doing this?
Because I need it when I’m developing a framework.
The framework I developed, with an annotation, when the user enters a variable name, class name, my framework can automatically generate an object for it, and load it into memory for later use.
This is a tricky little feature.
Initially I wanted to use reflection to do this, but I was almost done when I had a problem:
You can’t generate a class in real time, dynamically. Reflection can edit only one object.
Later, after various aspects of research, using the Javassist class bytecode to manipulate the class library.
Sure enough, the door opened. A success.
Comb it out now for later use.
The article directories
- Why am I doing this?
- Introduction to Javassist
-
- (A) What is Javassist
- Javassist core API
- (3) Simple examples
- Examples of Javassist manipulating bytecode
-
- (1) Add a method
- (2) Add a variable
- (3) add get and set methods to variables
- (4) add code inside the method
- (5) Insert code before and after the method body
- 3. Examples of some special parameters in Javassist
-
- (a) $0, $1, $2,…
- (2) $args
- (3) the $$
- (4) $cflow (…).
- A link to the
Introduction to Javassist
(A) What is Javassist
Javassist is a class library for dynamically editing Java bytecode. It can define a new class while the Java program is running and load it into the JVM; You can also modify a class file while the JVM is loading. Javassist allows users to edit class files without having to worry about bytecode-related specifications.
Javassist core API
In Javassist, each class that needs to be edited corresponds to an instance of CtCLass, which stands for compile time class, These classes are stored in a Class Pool (Class Poll is a container for storing CtClass objects). CtField and CtMethod in CtClass correspond to fields and methods in Java respectively. The CtClass object allows you to add fields and change methods to a class.
(3) Simple examples
In order to reduce the complexity of the demo, the samples and the rest of the operation are all done under the Maven project, because we can import the dependencies directly to achieve our package import purpose, rather than downloading jar packages and importing them manually.
1. Create a Maven project
If you’re using IDEA, you can do what I did; For other tools, you can search for them or create them based on your own experience.
Create a test class as follows:
package com.ssdmbbl.javassist;
import javassist.*;
import java.io.IOException;
/ * * *@author zhenghui
* @description: Javassist uses demo tests *@date2021/4/6 6:38pm */
public class JavassistTest {
public static void main(String[] args) throws CannotCompileException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
ctClass.writeFile(". /"); }}Copy the code
When you run this code, you can see that a package “com.ssdmbbl.javassist” has been created in the root directory of the project, where the Java file “hello.java” has been created.
As follows:
package com.ssdmbbl.javassist;
public class Hello {
public Hello(a) {}}Copy the code
Examples of Javassist manipulating bytecode
If we think back to a normal operation on Java, what operations might exist?
- 1. We will add a field to a class;
- 2. We will add methods to a class;
I don’t think there’s anything else. The rest is just code in the method.
(1) Add a method
Let’s continue with the code from the simple example above and add a method based on this.
The new method is named “hello1”, takes two arguments of type int and double, and returns no value.
package com.ssdmbbl.javassist;
import javassist.*;
import java.io.IOException;
import java.net.URL;
/ * * *@author zhenghui
* @description: Javassist uses demo tests *@date2021/4/6 6:38pm */
public class JavassistTest2 {
public static void main(String[] args) throws CannotCompileException, IOException {
// Find the path to this file and save it with it
URL resource = JavassistTest2.class.getClassLoader().getResource("");
String file = resource.getFile();
System.out.println("File storage path:"+file);
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
// Create a class named "hello" that passes arguments in the order of (int,double) and returns no value
/* CtMethod (...) The source code: Public CtMethod(CtClass returnType,// the returnType of this method, String mname, // (method name) what is the name of the method CtClass[] parameters, // what is the parameter type of the method CtClass variable) {.... } * /
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
// Set the hello method's permission to public
ctMethod.setModifiers(Modifier.PUBLIC);
// Add this method to ctClass
ctClass.addMethod(ctMethod);
// Write to localctClass.writeFile(file); }}Copy the code
After executing, you can view the generated code:
As you can see, we do not specify the name of the parameter, but will give the generated var1, var2 and so on.
Var1 and var2 are actually the names stored in the class variable table.
package com.ssdmbbl.javassist;
public class Hello {
public void hello1(int var1, double var2) {}public Hello(a) {}}Copy the code
The types of return values that can be set:
public static CtClass booleanType;
public static CtClass charType;
public static CtClass byteType;
public static CtClass shortType;
public static CtClass intType;
public static CtClass longType;
public static CtClass floatType;
public static CtClass doubleType;
public static CtClass voidType;
Copy the code
(2) Add a variable
URL resource = JavassistTest2.class.getClassLoader().getResource("");
String file = resource.getFile();
System.out.println("File storage path:"+file);
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
// Add a hello1 method
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctClass.addMethod(ctMethod);
// Add a variable of type int named value
CtField ctField = new CtField(CtClass.intType,"value",ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField);
ctClass.writeFile(file);
Copy the code
This is what happens after execution:
package com.ssdmbbl.javassist;
public class Hello {
private int value;
public void hello1(int var1, double var2) {}public Hello(a) {}}Copy the code
(3) add get and set methods to variables
The code is modified as follows:
public static void main(String[] args) throws CannotCompileException, IOException {
URL resource = JavassistTest2.class.getClassLoader().getResource("");
String file = resource.getFile();
System.out.println("File storage path:"+file);
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("com.ssdmbbl.javassist.Hello");
// Add a hello1 method
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctClass.addMethod(ctMethod);
// Add a variable of type int named value
CtField ctField = new CtField(CtClass.intType,"value",ctClass);
ctField.setModifiers(Modifier.PRIVATE);
ctClass.addField(ctField);
// Add a set method to the value variable
CtMethod setValue = new CtMethod(CtClass.voidType, "setValue".new CtClass[]{CtClass.intType}, ctClass);
setValue.setModifiers(Modifier.PUBLIC);
ctClass.addMethod(setValue);
// Add the get method to the value variable
CtMethod getValue = new CtMethod(CtClass.intType, "getValue".new CtClass[]{}, ctClass);
getValue.setModifiers(Modifier.PUBLIC);
ctClass.addMethod(getValue);
ctClass.writeFile(file);
}
Copy the code
Execution effect:
package com.ssdmbbl.javassist;
public class Hello {
private int value;
public void hello1(int var1, double var2) {}public void setValue(int var1) {}public int getValue(a) {}public Hello(a) {}}Copy the code
(4) add code inside the method
If you’re curious, there’s no code inside the set and get methods, so when the program runs, it’s bound to fail. Let’s take a look.
Our expected results:
private int value;
public void setValue(int var1) {
this.value = var1
}
public int getValue(a) {
return this.value;
}
Copy the code
Modified as follows:
// Add a set method to the value variable
CtMethod setValue = new CtMethod(CtClass.voidType, "setValue".new CtClass[]{CtClass.intType}, ctClass);
setValue.setModifiers(Modifier.PUBLIC);
// Set the method body
setValue.setBody("this.value = var1;");
ctClass.addMethod(setValue);
// Add the get method to the value variable
CtMethod getValue = new CtMethod(CtClass.intType, "getValue".new CtClass[]{}, ctClass);
getValue.setModifiers(Modifier.PUBLIC);
// Set the method body
getValue.setBody("return this.value;");
ctClass.addMethod(getValue);
Copy the code
Unfortunately, I can’t find the variable var1
The stack message is as follows:
Exception in thread "main" javassist.CannotCompileException: [source error] no such field: var1
at javassist.CtBehavior.setBody(CtBehavior.java:474)
at javassist.CtBehavior.setBody(CtBehavior.java:440)
at com.ssdmbbl.javassist.JavassistTest2.main(JavassistTest2.java:41)
Caused by: compile error: no such field: var1
Copy the code
We actually mentioned this earlier, because at compile time, the variable name is erased, and the parameters passed are in the order of the local variable table.
If passed:
public void test(int a,int b,int c){... }Copy the code
So a, B, and C correspond to positions 1,2, and 3 in the local variable table.
—— —— | a |1| | | b2| | | c3| -- -Copy the code
So instead of using the original name when we get variables, we use $1, 2, 2, 2… to access the parameters in the method in Javassist. Instead of using the original name.
We modify as follows:
// Add a set method to the value variable
CtMethod setValue = new CtMethod(CtClass.voidType, "setValue".new CtClass[]{CtClass.intType}, ctClass);
setValue.setModifiers(Modifier.PUBLIC);
// Set the method body
setValue.setBody("this.value = $1;");
ctClass.addMethod(setValue);
// Add the get method to the value variable
CtMethod getValue = new CtMethod(CtClass.intType, "getValue".new CtClass[]{}, ctClass);
getValue.setModifiers(Modifier.PUBLIC);
// Set the method body
getValue.setBody("return this.value;");
ctClass.addMethod(getValue);
Copy the code
It worked:
public class Hello {
private int value;
public void hello1(int var1, double var2) {}public void setValue(int var1) {
this.value = var1;
}
public int getValue(a) {
return this.value;
}
public Hello(a) {}}Copy the code
One more: modify the body of the hello1 method to add the two parameters passed and assign them to value;
// Add a hello1 method
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("this.value = $1 + $2;");
ctClass.addMethod(ctMethod);
Copy the code
The result is as follows:
Because value is an int, $1 is an int, and $2 is a double, we cast it.
public void hello1(int var1, double var2) {
this.value = (int) ((double)var1 + var2);
}
Copy the code
(5) Insert code before and after the method body
The test code is as follows:
// Add a hello1 method
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("this.value = $1 + $2;");
ctMethod.insertBefore("System.out.println(\" I inserted: \"+$1);");
ctMethod.insertAfter("System.out.println(\" I inserted: \"+$1);");
ctClass.addMethod(ctMethod);
Copy the code
The results are as follows:
public void hello1(int var1, double var2) {
System.out.println("I inserted in front:" + var1);
this.value = (int) ((double)var1 + var2);
Object var5 = null;
System.out.println("I inserted at the end:" + var1);
}
Copy the code
3. Examples of some special parameters in Javassist
In www.javassist.org/tutorial/tu… There are a few more special identifiers you can see in the official documentation, and a few more special identifiers you need to know about.
identifier | role |
---|---|
1, $2, 3, 3, 3… |
This and method arguments (1-n is the order of method arguments) |
$args | Method parameter array of type Object[] |
$$ | All method arguments, such as: m($$) equals m( 2,…). |
$cflow (…). | The control flow variables |
$r | The type of result returned, used in cast expressions. |
$w | Wrapper type, used in cast expressions. |
The $_ | The result value returned |
$sig | An array of parameter type objects of type java.lang.Class |
$type | A return value type of type java.lang.Class |
$class | The Class being modified of type java.lang.Class |
Now let’s take a look at them separately, analyze how to use them and what to use them for.
Let’s just introduce a few common and commonly used ones. Are interested, and the rest we can see the official document: www.javassist.org/tutorial/tu…
(a) $0, $1, $2,…
We’ve actually used this already, but let’s talk about it a little bit more. $0 = this, $1, 2, 2, 2… Corresponding to the sequence of parameters in the method. If you have:
public void test(int a,int b,int c){}
Copy the code
So if you want to reference a and B and c, you need to do this:
ctMethod.setBody("return $1 + $2 + $3;");
Copy the code
Static methods do not have $0, so $0 is not available for static methods.
(2) $args
The $args variable represents an array of all parameters. It is an Object array (new Object[]{… }), if there is an argument of the original type, it will be converted to the corresponding wrapper type. For example, if the original data type is int, it will be converted to java.lang.Integer and stored in ARGS.
For example, our test code is as follows:
CtMethod ctMethod = new CtMethod(CtClass.voidType, "hello1".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("System.out.println($args);");
Copy the code
The result is as follows:
public void hello1(int var1, double var2) {
System.out.println(new Object[]{new Integer(var1), new Double(var2)});
}
Copy the code
(3) the $$
A variable is an abbreviation of the number of arguments, which are separated by a comma. For example, m(an abbreviation of all arguments, which are separated by a comma, for example, m() is equivalent to m($1,$2,$3…). .
How do you use it?
We often do some code optimizations to extract the logic inside a more complex method into a common method.
Or when one method calls another, and the two methods pass the same arguments.
For example, the original Java:
public Object m1(String name,String age){... omit10000Line of code logic}Copy the code
Refined:
The extracted code can also be used elsewhere (so-called public code).
public Object m1(String name,String age){... omit20Line code logicreturn m2(name,age);
}
private Object m2(String name,String age){... }Copy the code
So let’s create this scenario to illustrate how it works.
In simple terms, when one method calls another, passing full arguments.
CtMethod hello2 = new CtMethod(CtClass.voidType, "hello2".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
hello2.setModifiers(Modifier.PUBLIC);
hello2.setBody("this.value = $1 + $2;");
ctClass.addMethod(hello2);
// Add a hello1 method
CtMethod hello1 = new CtMethod(CtClass.voidType, "hello1".new CtClass[]{CtClass.intType, CtClass.doubleType}, ctClass);
hello1.setModifiers(Modifier.PUBLIC);
hello1.setBody("this.value = $1 + $2;");
hello1.insertAfter("hello2($$);");
Copy the code
As you can see, when we call hello2 from hello1, we need to pass all the arguments. At this point you can write the $$method, or of course hello2($1,$2).
The compiled result:
public void hello2(int var1, double var2) {
this.value = (int) ((double)var1 + var2);
}
public void hello1(int var1, double var2) {
this.value = (int) ((double)var1 + var2);
Object var5 = null;
this.hello2(var1, var2);
}
Copy the code
(4) $cflow (…).
$cflow, full name: Control flow, is a read-only variable that returns the depth of recursive calls to the specified method.
Let’s use the Fibonacci column of n as an example to demonstrate how to use it.
Our correct recursive algorithm code is as follows:
public int f(int n){
if(n <= 1) {return n;
}
return f(n-1) + f(n - 2)}Copy the code
For the above code, we can write:
CtMethod f = new CtMethod(CtClass.intType,"f".new CtClass[]{CtClass.intType}, ctClass);
f.setBody("{if($1 <= 1){" +
" return $1;" +
"}" +
"return f($1 - 1) + f( $1 - 2); }");
f.setModifiers(Modifier.PUBLIC);
Copy the code
Compiled:
public int f(int var1) {
return var1 <= 1 ? var1 : this.f(var1 - 1) + this.f(var1 - 2);
}
Copy the code
So we just want to print the log for the first n recursions, so what do we do.
For example, we write “$cflow(f) == 1”
CtMethod f = new CtMethod(CtClass.intType,"f".new CtClass[]{CtClass.intType}, ctClass);
f.setBody("{if($1 <= 1){" +
" return $1;" +
"}" +
"return f($1 - 1) + f( $1 - 2); }");
f.setModifiers(Modifier.PUBLIC);
// Insert before the body
f.useCflow("f");
f.insertBefore("if($cflow(f) == 1){" +
"System.out.println(\" I execute, n is: \"+$1);" +
"}");
Copy the code
Compiled code:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.ssdmbbl.javassist;
import javassist.runtime.Cflow;
public class Hello {
public static Cflow _cflow$0 = new Cflow();
public int f(int var1) {
if (_cflow$0.value() == 1) {
System.out.println("I execute for the second time, where n is:" + var1);
}
boolean var6 = false;
int var10000;
try {
var6 = true;
_cflow$0.enter();
if (var1 <= 1) {
var10000 = var1;
var6 = false;
} else {
var10000 = this.f(var1 - 1) + this.f(var1 - 2);
var6 = false; }}finally {
if (var6) {
boolean var3 = false;
_cflow$0.exit(); }}int var8 = var10000;
_cflow$0.exit();
return var8;
}
public Hello(a) {}}Copy the code
Looking at the source code, one of the key points is to call the Cflow object’s Enter method:
public static Cflow _cflow$0 = newCflow(); . _cflow$0.enter();
Copy the code
Click on the internal discovery of Enter () and call the get().inc() method:
public class Cflow extends ThreadLocal<Cflow.Depth> {
protected static class Depth {
private int depth;
Depth() { depth = 0; }
int value(a) { return depth; }
void inc(a) { ++depth; }
void dec(a) { --depth; }}@Override
protected synchronized Depth initialValue(a) {
return new Depth();
}
/** * Increments the counter. */
public void enter(a) { get().inc(); }
/** * Decrements the counter. */
public void exit(a) { get().dec(); }
/** * Returns the value of the counter. */
public int value(a) { returnget().value(); }}Copy the code
The inc() method controls the increment of a global variable.
void inc(a) { ++depth; }
Copy the code
boolean var6 = false; It’s like a switch that starts and ends.
The end occurs when var1<=1
if (var1 <= 1) {
var10000 = var1;
var6 = false;
}
Copy the code
We can use reflection to verify this. The test code is as follows:
package com.ssdmbbl.javassist;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/ * * *@author zhenghui
* @description:
* @date 2021/4/8 10:20 上午
*/
public class Test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { Class<? > aClass = Class.forName("com.ssdmbbl.javassist.Hello");
// Initialize the class
Object obj = aClass.newInstance();
// Get all the methods
Method[] methods = aClass.getMethods();
// Iterate over all the methods
for (Method method : methods) {
// call when the method is f
if(method.getName().equals("f")) {// Call and pass the argument 5, that is, f(5)
method.invoke(obj,5); }}}}Copy the code
Results of execution:
Why 2? If (var1 <= 1){if(var1 <= 1){ }, so
I did it, and now n is:4I did it, and now n is:3
Copy the code
A link to the
- Javassist API documentation
www.javassist.org/html/index….
- 2. Javassist’s Github open source address
Github.com/jboss-javas…
- Javassist’s official website
www.javassist.org/
- 4. Javassist official English tutorial
www.javassist.org/tutorial/tu…