asm不是一个新的东西,javaee领域的开源框架都有asm的用武之地。准确来说 asm是用来操作字节码的,源代码是java编写。
asm官网 https://asm.ow2.io/index.html
asm的使用稍微复杂,需要了解字节码。我强烈建议从事java开发的同学必须会asm的基本操作,这会让你非常容易接近jvm的编译指令,类加载等原理上的东西,便于更好的理解jvm与java特性。
一、创建一个字节码的例子
源代码 https://github.com/liuchengts/asm-demo
下面是核心代码:
/**
* 创建class并且加载到内存
*
* @param name 要创建的class名称 相当于 class.getName()
* @param interfaceClasss 要实现的的接口class,可以是null
* @param extendsClasssImpl 要继承的类class
* @param isInterface 是一个接口true
* @return 返回创建好的class对象
*/
public static Class createClass(String name, Set<Class> interfaceClasss, Class extendsClasssImpl, boolean isInterface) {
String classPath = name.replace(".", File.separator);
//定义class书写器
ClassWriter cw = new ClassWriter(0);
if (extendsClasssImpl == null) {
//默认继承顶级 Object 类
extendsClasssImpl = Object.class;
}
if (interfaceClasss == null || interfaceClasss.isEmpty()) {
//第一个参数 V1_1 是生成的class的版本号, 对应class文件中的主版本号和次版本号, 即minor_version和major_version
//第二个参数 ACC_PUBLIC 表示该类的访问标识。这是一个public的类。 对应class文件中的access_flags
//第三个参数是生成的类的类名。 需要注意,这里是类的全限定名。 如果生成的class带有包名, 如com.jg.zhang.Example, 那么这里传入的参数必须是com/jg/zhang/Example 。对应class文件中的this_class
//第四个参数是和泛型相关的, 这里我们不关新, 传入null表示这不是一个泛型类。这个参数对应class文件中的Signature属性(attribute)
//第五个参数是当前类的父类的全限定名。 没有的话默认应该继承Object。 这个参数对应class文件中的super_class
//第六个参数是String[]类型的, 传入当前要生成的类的直接实现的接口。 这里这个类没实现任何接口, 所以传入null 。 这个参数对应class文件中的interfaces
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, classPath, null, Type.getInternalName(extendsClasssImpl), null);
} else {
Set<String> interfaceSet = new HashSet<>();
for (Class las : interfaceClasss) {
interfaceSet.add(Type.getInternalName(las));
}
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, classPath, null, Type.getInternalName(extendsClasssImpl), interfaceSet.toArray(new String[interfaceSet.size()]));
}
//生成默认的构造方法
//第一个参数是 ACC_PUBLIC , 指定要生成的方法的访问标志。 这个参数对应method_info 中的access_flags 。
//第二个参数是方法的方法名。 对于构造方法来说, 方法名为<init> 。 这个参数对应method_info 中的name_index , name_index引用常量池中的方法名字符串。
//第三个参数是方法描述符, 在这里要生成的构造方法无参数, 无返回值, 所以方法描述符为 ()V 。 这个参数对应method_info 中的descriptor_index 。
//第四个参数是和泛型相关的, 这里传入null表示该方法不是泛型方法。这个参数对应method_info 中的Signature属性。
//第五个参数指定方法声明可能抛出的异常。 这里无异常声明抛出, 传入null 。 这个参数对应method_info 中的Exceptions属性
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC,
"<init>",
"()V",
null,
null);
//生成构造方法的字节码指令
//1 调用visitVarInsn方法,生成aload指令, 将第0个本地变量(也就是this)压入操作数栈。
//2 调用visitMethodInsn方法, 生成invokespecial指令, 调用父类(也就是Object)的构造方法。
//3 调用visitInsn方法,生成return指令, 方法返回。
//4 调用visitMaxs方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应Code属性中的max_stack和max_locals 。
//5 最后调用visitEnd方法, 表示当前要生成的构造方法已经创建完成。
mw.visitVarInsn(Opcodes.ALOAD, 0);
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(extendsClasssImpl), "<init>", "()V", isInterface);
mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
//生成mian方法
mw = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
"main",
"([Ljava/lang/String;)V",
null,
null);
//生成main方法中的字节码指令
mw.visitFieldInsn(Opcodes.GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;");
mw.visitLdcInsn("Hello world!");
mw.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V", isInterface);
// 获取生成的class文件对应的二进制流
byte[] code = cw.toByteArray();
// 写文件到本地
String classFile = writerFile(code, classPath);
Map map = new HashMap();
map.put(name, classFile);
mapThreadLocal.set(map);
//直接将二进制流加载到内存中
return ASMUtils.getInstance().defineClass(name, code, 0, code.length);
}
调用:
public static void main(String[] args) {
String name = "com.lc.study.Test";//这是一个不存在的包和类
//将这个类继承 TestServiceImpl 并且标记这不是一个接口
Class test = ASMUtils.createClass(name, null, TestServiceImpl.class, false);
try {
String classFile = ASMUtils.getClassFilePath(name);
System.out.println("class文件位置:" + classFile);
System.out.println("************* 获取到的当前类(包括父类的)所有方法");
for (Method method : test.getMethods()) {
System.out.println("> 方法:" + method.getName());
Class[] parameterTypes = method.getParameterTypes();
System.out.println(">>> 入参数量:" + parameterTypes.length);
for (Class c : parameterTypes) {
System.out.println(">>>>>> 参数:" + c.getSimpleName());
}
System.out.println(">>> 返回类型:" + method.getReturnType().getSimpleName());
Annotation[] annotations = method.getDeclaredAnnotations();
System.out.println(">>> 方法注解数量:" + annotations.length);
for (Annotation a : annotations) {
System.out.println(">>>>>> 注解:" + a.toString());
}
}
System.out.println("************* 打印完成,开始方法调用测试");
//调用当前类的main方法
test.getDeclaredMethod("main", new String[]{}.getClass()).invoke(null, new Object[]{null});
//调用父类中的的方法
//先实例化,创建一个对象
Object object = test.newInstance();
//调用父类的 getTest 方法
Method getTest = test.getMethod("getTest");
getTest.invoke(object);
//调用父类的 updateTest 方法
Method updateTest = test.getMethod("updateTest", Integer.class);
Object obj = updateTest.invoke(object, new Integer[]{1});
System.out.println("updateTest 返回参数:" + obj);
} catch (Exception e) {
e.printStackTrace();
}
}
这里会生成一个class文件,当然可以选择不生成。
假设 classFile打印输出的是 /a/b/c/Test.class
打开这个Test.class 可以看到原始代码
执行./shell.sh /a/b/c/Test.class 反编译回来看看字节码
asm还可以创建字段 注解等,考虑到asm偏于底层,已经有很多上层扩展包封装创建好了 所以这里不过多演示。