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相对简单一点。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)