使用javassist修改class文件内方法
- 在工作突然有一个需求。线上运维的一个tomcat的web项目,运行的程序不正常。需要修改代码。可是这个项目代码非常的老,并且公司存储的源代码跟线上的不一致。
- 我了个擦,没有源代码但是还要结局客户的问题。只能到线上将对应程序的class文件拷贝到本地进行修改,每修改一部分就上传到线上覆盖掉之前的class文件,重启tomcat进行测试。(过程想当麻烦)
- 修改class字节码文件用到 IDEA工具来反编译class进行查看代码,javassist工具进行修改。
- 修改method中的方法时,主要是对 书写的代码 格式有很多要求
javasssist的主要功能
前言
- Javassist是日本人开发的一款编辑class字节码框架,可以用来检查、动态修改及创建Java类。与JDK自带的反射功能相比Javassist功能更加强大,熟练使用Javassist工具对提高Java动态编程有着重要意义。
功能
-
官网地址: javassist官网
-
主要的类:
- ClassPool // 操作前都要创建一个 class痴
- CtClass // 具体的操作class的对象
- CtMethod // class文件中的 某个方法对象
- CtField // class文件中的 某个方法字段
-
主要的方法
- CtClass.addMethod
- CtClass.removeMethod
- CtClass.removeField
- CtClass.writeFile // 写入文件,
- CtClass.addField
- CtMethod.insertBefore
- CtMethod.insertAfter
- CtMethod.insertAt
- CtMethod.setBody
简单使用
- 这些只是一些简单的使用方法,因为某些没有源代码,而要修改一小部分线上class代码时,就可以使用。更高深的动态编程 。 俺不会,(怎么java的东西这么多,感觉每个都好深,好复杂。555~)
修改class文件中的某个方法
-
此方法只能整体修改 method的所有内容。目前没有找到可以局部修改代码的方法。
-
先下载jar包/或导入maven
-
<!-- https://mvnrepository.com/artifact/org.javassist/javassist --> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency>
-
写一个 main方法,操作对应 class文件
package com.lucumt; import java.io.IOException; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.NotFoundException; public class UpdateMethod { private static String pathName = "D:\\Java\\xxxxx\\test\\bin"; private static String className = "com.lucumt.Test1"; public static void main(String[] args) { updateMethod(); } public static void updateMethod(){ try { ClassPool cPool = new ClassPool(true); //如果该文件引入了其它类,需要利用类似如下方式声明 //cPool.importPackage("java.util.List"); //设置class文件的位置 cPool.insertClassPath(pathName); // 导入需要引入的 包 cPool.importPackage("com.gdzy.JZFW.service"); cPool.importPackage("java.text"); cPool.importPackage("com.gdzy.JZFW.pojo"); cPool.importPackage("com.gdzy.JZFW.util"); cPool.importPackage("java.net"); cPool.importPackage("java.util"); cPool.importPackage("javax.servlet"); cPool.importPackage("com.sun.syndication"); //获取该class对象 CtClass cClass = cPool.get(className); //获取到对应的方法 CtMethod cMethod = cClass.getDeclaredMethod("addNumber"); //更改该方法的内部实现 //需要注意的是对于参数的引用要以$开始,不能直接输入参数名称 cMethod.setBody("{ "long z1 = System.currentTimeMillis();\n"+ " System.out.println(\"1111111111111111111111--------------------------------------------------------\");\n"+ " boolean sendOld = false;\n"+ " java.util.Map/*<String, Object>*/ mapparam = new java.util.HashMap();\n" + " mapparam.put(\"typenameEqual\", \"old_sendEQIM_used\");\n" + " java.util.List/*<com.gdzy.JZFW.pojo.Useruse>*/ listOldused = this.useruseService.selectList(mapparam);\n"+ " if (listOldused.size() > 0 && ((com.gdzy.JZFW.pojo.Useruse)listOldused.get(0)).getParametervalues().equals(\"1\")) {\n" + " sendOld = true;\n" + " }\n"+ " boolean sendNew = false;\n" + " mapparam.clear();\n" + " mapparam.put(\"typenameEqual\", \"new_sendEQIM_used\");\n" + " System.out.println(\"222222222222222222222222-----------------------------------\");\n" + " java.util.List/*<com.gdzy.JZFW.pojo.Useruse>*/ listNewused = this.useruseService.selectList(mapparam);\n" + " if (listNewused.size() > 0 && ((com.gdzy.JZFW.pojo.Useruse)listNewused.get(0)).getParametervalues().equals(\"1\")) {\n" + " sendNew = true;\n" + " }\n"+ "............." }"); //替换原有的文件 cClass.writeFile(pathName); System.out.println("=======change finish========="); } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
写method的 body代码注意点:
-
如果方法要使用 参数的话 不能直接 使用 。 需要用 $1,$2,$3 来代替。 1-2-3代表前后顺序
列如: 方法上有 (int a,int b) a和b两个参数 那么在 setBody方法中要 使用 a 和 b 就需要用 $1=>a $2=>b 使用$1,$2来代替 , $0代码的是this $args :$args 指的是方法所有参数的数组类似Object[],需要注意$args[0]对应的是$1,而不是$0 $r:指的是方法返回值的类型,主要用在类型的转型上 $w:$w代表一个包装类型。主要用在转型上。比如:Integer i = ($w)5;如果该类型不是基本类型,则会忽略 $type:返回结果值的类型
-
在写入某些对象时要加上包全路径名称
列如: Date , List , Map , 还有一些自定义的pojo类或者 service // 第三方自己定义的 service 和 pojo 可以在最开始通过 cPool.importPackage("com.gdzy.JZFW.service"); 来进行导入
-
在使用<>这样的泛型定义 标识时 要使用 /* */ 将其包括起来
列如: List<String> 要写成 List/*<String>*/
-
还有一个问题是我需改的class文件需要再重新上线到tomcat中运行,但是我每次修改完运行时都会报一个 线程没有正常结束 的错误,导致tomcat启动不了,最后发现是某些类型的定义不能使用 引用类型 只能 使用 基本类型
列如: Float,Long 不能使用 Float.valueOf() 只能使用 Float.parseFloat() 来进行转换类型
在Class文件中增加方法
-
利用Javassist增加方法比修改方法更简单,先将要新增的方法内容赋值到字符串,然后分别调用相关类的 make 和 addMethod 方法即可 。
public static void addMethod(){ try { ClassPool cPool = new ClassPool(true); cPool.insertClassPath(pathName); CtClass cClass = cPool.get(className); CtMethod cMethod = cClass.getDeclaredMethod("addNumber"); //增加一个新方法 String methodStr ="public void showParameters(int a,int b){" +" System.out.println(\"First parameter: \"+a);" +" System.out.println(\"Second parameter: \"+b);" +"}"; CtMethod newMethod = CtNewMethod.make(methodStr, cClass); cClass.addMethod(newMethod); //调用新增的方法 cMethod.setBody("{ showParameters($1,$2);return $1*$1*$1+$2*$2*$2; }"); cClass.writeFile(pathName); } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
在Class文件中增加成员变量
public static void addField(){
try {
ClassPool cPool = new ClassPool(true);
cPool.insertClassPath(pathName);
CtClass cClass = cPool.get(className);
//增加一个新成员变量
cClass.addField(CtField.make("private String str;",cClass));
cClass.writeFile(pathName);
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}