如何使用javassist修改已经加载的类的方法

如何使用javassist修改已经加载的类的方法,第1张

参考手册:

1、读取和输出字节码

ClassPool pool = ClassPool.getDefault()

//会从classpath中查询该类

CtClass cc = pool.get("test.Rectangle")

//设置.Rectangle的父类

cc.setSuperclass(pool.get("test.Point"))

//输出.Rectangle.class文件到该目录中

cc.writeFile("c://")

//输出成二进制格式

//byte[] b=cc.toBytecode()

//输出并加载class 类,默认加载到当前线程的ClassLoader中,也可以选择输出的ClassLoader。

//Class clazz=cc.toClass()

这里可以看出,Javassist的加载是依靠ClassPool类,输出方式支持三种。

2、新增Class

ClassPool pool = ClassPool.getDefault()

CtClass cc = pool.makeClass("Point")

//新增方法

cc.addMethod(m)

//新增Field

cc.addField(f)

从上面可以看出,对Class的修改主要是依赖于CtClass 类。API也比较清楚和简单。

3、冻结Class

当CtClass 调用writeFile()、toClass()、toBytecode() 这些方法的时候,Javassist会冻结CtClass Object,对CtClass object的修改将不允许。这个主要是为了警告开发者该类已经被加载,而JVM是不允许重新加载该类的。如果要突破该限制,方法如下:

CtClasss cc = ...

:

cc.writeFile()

cc.defrost()

cc.setSuperclass(...) // OK since the class is not frozen.

当 ClassPool.doPruning=true的时 候,Javassist 在CtClass object被冻结时,会释放存储在ClassPool对应的数据。这样做可以减少javassist的内存消耗。默认情况 ClassPool.doPruning=false。例如

CtClasss cc = ...

cc.stopPruning(true)

:

cc.writeFile()// convert to a class file.

// cc没有被释放

提示:当调试时,可以调用debugWriteFile(),该方法不会导致CtClass被释放。

4、Class 搜索路径

从上面可以看出Class 的载入是依靠ClassPool,而ClassPool.getDefault() 方法的搜索Classpath 只是搜索JVM的同路径下的class。当一个程序运行在JBoss或者Tomcat下,ClassPool Object 可能找到用户的classes。Javassist 提供了四种动态加载classpath的方法。如下

//默认加载方式如pool.insertClassPath(new ClassClassPath(this.getClass()))

ClassPool pool = ClassPool.getDefault()

//从file加载classpath

pool.insertClassPath("/usr/local/javalib")

//从URL中加载

ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.")

pool.insertClassPath(cp)

//从byte[] 中加载

byte[] b = a byte array

String name = class name

cp.insertClassPath(new ByteArrayClassPath(name, b))

//可以从输入流中加载class

InputStream ins = an input stream for reading a class file

CtClass cc = cp.makeClass(ins)

5、ClassPool

5.1 减少内存溢出

ClassPool是一个CtClass objects的装载容器。当加载了CtClass object后,是不会被ClassPool释放的(默认情况下)。这个是因为CtClass object 有可能在下个阶段会被用到。

当加载过多的CtClass object的时候,会造成OutOfMemory的异常。为了避免这个异常,javassist提供几种方法,一种是在上面提到 的 ClassPool.doPruning这个参数,还有一种方法是调用CtClass.detach()方法,可以把CtClass object 从ClassPool中移除。例如:

CtClass cc = ...

cc.writeFile()

cc.detach()

另外一中方法是不用默认的ClassPool即不用 ClassPool.getDefault()这个方式来生成。这样当ClassPool 没被引用的时候,JVM的垃圾收集会收集该类。例如

//ClassPool(true) 会默认加载Jvm的ClassPath

ClassPool cp = new ClassPool(true)

// if needed, append an extra search path by appendClassPath()

5.2 级联ClassPools

javassist支持级联的ClassPool,即类似于继承。例如:

ClassPool parent = ClassPool.getDefault()

ClassPool child = new ClassPool(parent)

child.insertClassPath("./classes")

5.3 修改已有Class的name以创建一个新的Class

当调用setName方法时,会直接修改已有的Class的类名,如果再次使用旧的类名,则会重新在classpath路径下加载。例如:

ClassPool pool = ClassPool.getDefault()

CtClass cc = pool.get("Point")

cc.setName("Pair")

//重新在classpath加载

CtClass cc1 = pool.get("Point")

对于一个被冻结(Frozen)的CtClass object ,是不可以修改class name的,如果需要修改,则可以重新加载,例如:

ClassPool pool = ClassPool.getDefault()

CtClass cc = pool.get("Point")

cc.writeFile()

//cc.setName("Pair") wrong since writeFile() has been called.

CtClass cc2 = pool.getAndRename("Point", "Pair")

6、Class loader

上面也提到,javassist同个Class是不能在同个ClassLoader中加载两次的。所以在输出CtClass的时候需要注意下,例如:

// 当Hello未加载的时候,下面是可以运行的。

ClassPool cp = ClassPool.getDefault()

CtClass cc = cp.get("Hello")

Class c = cc.toClass()

//下面这种情况,由于Hello2已加载,所以会出错

Hello2 h=new Hello2()

CtClass cc2 = cp.get("Hello2")

Class c2 = cc.toClass()//这里会抛出java.lang.LinkageError 异常

//解决加载问题,可以指定一个未加载的ClassLoader

Class c3 = cc.toClass(new MyClassLoader())

6.1 使用javassist.Loader

从上面可以看到,如果在同一个ClassLoader加载两次Class抛出异常,为了方便javassist也提供一个Classloader供使用,例如

ClassPool pool = ClassPool.getDefault()

Loader cl = new Loader(pool)

CtClass ct = pool.get("test.Rectangle")

ct.setSuperclass(pool.get("test.Point"))

Class c = cl.loadClass("test.Rectangle")

Object rect = c.newInstance()

:

为了方便监听Javassist自带的ClassLoader的生命周期,javassist也提供了一个listener,可以监听ClassLoader的生命周期,例如:

//Translator 为监听器

public class MyTranslator implements Translator {

void start(ClassPool pool)

throws NotFoundException, CannotCompileException {}

void onLoad(ClassPool pool, String classname)

throws NotFoundException, CannotCompileException

{

CtClass cc = pool.get(classname)

cc.setModifiers(Modifier.PUBLIC)

}

}

//示例

public class Main2 {

public static void main(String[] args) throws Throwable {

Translator t = new MyTranslator()

ClassPool pool = ClassPool.getDefault()

Loader cl = new Loader()

cl.addTranslator(pool, t)

cl.run("MyApp", args)

}

}

//输出

% java Main2 arg1 arg2...

6.2 修改系统Class

由JVM规范可知,system classloader 是比其他classloader 是优先加载的,而system classloader 主要是加载系统Class,所以要修改系统Class,如果默认参数运行程序是不可能修改的。如果需要修改也有一些办法,即在运 行时加入-Xbootclasspath/p: 参数的意义可以参考其他文件。下面修改String的例子如下:

ClassPool pool = ClassPool.getDefault()

CtClass cc = pool.get("java.lang.String")

CtField f = new CtField(CtClass.intType, "hiddenValue", cc)

f.setModifiers(Modifier.PUBLIC)

cc.addField(f)

cc.writeFile(".")

//运行脚本

% java -Xbootclasspath/p:. MyApp arg1 arg2...

6.3 动态重载Class

如果JVM运行时开启JPDA(Java Platform Debugger Architecture),则Class是运行被动态重新载入的。具体方式可以参考java.lang.Instrument。javassist也提 供了一个运行期重载Class的方法,具体可以看API 中的javassist.tools.HotSwapper。

7、Introspection和定制

javassist封装了很多很方便的方法以供使用,大部分使用只需要用这些API即可,如果不能满足,Javassist也提供了一个低层的API(具体参考javassist.bytecode 包)来修改原始的Class。

7.1 插入source 文本在方法体前或者后

CtMethod 和CtConstructor 提供了 insertBefore()、insertAfter()和 addCatch()方法,它们可以插入一个souce文本到存在的方法的相应的位置。javassist 包含了一个简单的编译器解析这souce文本成二进制插入到相应的方法体里。

javassist 还支持插入一个代码段到指定的行数,前提是该行数需要在class 文件里含有。

插入的source 可以关联fields 和methods,也可以关联方法的参数。但是关联方法参数的时,需要在程序编译时加上 -g 选项(该选项可以把本地变量的声明保存在class 文件中,默认是不加这个参数的。)。因为默认一般不加这个参数,所以Javassist也提供了一些特殊的变量来代表方法参 数:$1,$2,$args...要注意的是,插入的source文本中不能引用方法本地变量的声明,但是可以允许声明一个新的方法本地变量,除非在程序 编译时加入-g选项。

方法的特殊变量说明:

$0, $1, $2, ... this and actual parameters

$args An array of parameters. The type of $args is Object[].

$$ All actual parameters.For example, m($$) is equivalent to m($1,$2,...)

$cflow(...) cflow variable

$r The result type. It is used in a cast expression.

$w The wrapper type. It is used in a cast expression.

$_ The resulting value

$sig An array of java.lang.Class objects representing the formal parameter types

$type A java.lang.Class object representing the formal result type.

$class A java.lang.Class object representing the class currently edited.

7.1.1 $0, $1, $2, ...

$0代表的是this,$1代表方法参数的第一个参数、$2代表方法参数的第二个参数,以此类推,$N代表是方法参数的第N个。例如:

//实际方法

void move(int dx, int dy)

//javassist

CtMethod m = cc.getDeclaredMethod("move")

//打印dx,和dy

m.insertBefore("{ System.out.println($1)System.out.println($2)}")

注意:如果javassist改变了$1的值,那实际参数值也会改变。

7.1.2 $args

$args 指的是方法所有参数的数组,类似Object[],如果参数中含有基本类型,则会转成其包装类型。需要注意的时候,$args[0]对应的是$1,而不是$0,$0!=$args[0],$0=this。

7.1.3 $$

$$是所有方法参数的简写,主要用在方法调用上。例如:

//原方法

move(String a,String b)

move($$) 相当于move($1,$2)

如果新增一个方法,方法含有move的所有参数,则可以这些写:

exMove($$, context) 相当于 exMove($1, $2, context)

7.1.4 $cflow

$cflow意思为控制流(control flow),是一个只读的变量,值为一个方法调用的深度。例如:

//原方法

int fact(int n) {

if (n <= 1)

return n

else

return n * fact(n - 1)

}

12 import

以通过一种手段来在程序内部来修改授权部分的实现,使真实的授权部分隐藏在其它代码部分,而可视的授权代码并不参与实际的授权授权,这样的话,对于破解者来说,修改表向的代码实现并不能真正修改代码实现,因为真实的实现已经通过其它代码将原始实现替换掉了。

即在调用授权代码之前将授权原代码进行修改,然后调用授权代码时即调用已经修改后的授权代码,而真实的授权代码是查看不了的(通过某种方式注入),这样即达到一种授权方式的隐藏。

可以通过javassist来修改java类的一个方法,来修改一个方法的真实实现。修改的方法可以是动态方法,也可以是静态方法。修改的前提即是当前修改的类还没有被当前jvm加载,如果当前的类已经被加载,则不能修改。

ClassPool classPool = ClassPool.getDefault()

     CtClass ctClass = classPool.get("com.develop.Txt")

CtMethod ctMethod = ctClass.getDeclaredMethod("i")

ctMethod.setBody("{try{Integer i = null"

+ "int y = i.intValue()System.out.println(\"this is a new method\")")

ctClass.toClass()

上面的方法即是修改一个方法的实现,当调用ctClass.toClass()时,当前类即会被当前的classLoader加载,并实例化类。

需要注意的是,在调用ctClass.toClass()时,会加载此类,如果此类在之前已经被加载过,则会报一个duplicate load的错误,表示不能重复加载一个类。所以,修改方法的实现必须在修改的类加载之前进行。

    即使不调用toClass,那么如果当前修改的类已经加载,那么修改方法实现,同样不起作用,即修改一个已经加载的类(不论是修改静态方法,还是修改动态方法)是没有任何效果的。修改之前必须在类加载之前进行。

    当然,使用aspectj也可以同样达到修改的效果,不过修改指定的类,则需要为修改这个类添加一个aspect,然后将这个aspect加入配置文件中以使其生效,比起javassist来说,修改一个类还是使用javassist相对简单一点。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/bake/11534948.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-16
下一篇 2023-05-16

发表评论

登录后才能评论

评论列表(0条)

保存