了解下AOP中的Javassist

2,277

什么是Javassist

Javasisst是一个能让Java字节码操作变得简单的一个类库,用于在Java中编辑字节码;它使Java程序能够在运行时定义一个新类,并在JVM加载时修改类文件。与其他类似的字节码编辑器不同,Javassist提供了两个级别的API:源代码级和字节码级。如果用户使用源级API,他们可以在不知道Java字节码规范的情况下编辑类文件;整个API的设计只使用Java语言的词汇表您甚至可以以源文本的形式指定插入的字节码;另一方面,字节码级API允许用户像其他编辑器一样直接编辑类文件。

读取以及修改字节码

ClassPool pool = ClassPool.getDefault();


//step 1 : 创建一个类叫做 AnimalClass
CtClass cc = pool.makeClass("AnimalClass");


//step 2 : 新增一个name字段 类型String
CtField param = new CtField(pool.get("java.lang.String"),"name",cc);
param.setModifiers(Modifier.PRIVATE);
cc.addField(param,CtField.Initializer.constant("XiaoMao"));

// step 3 :新增get set方法
cc.addMethod(CtNewMethod.setter("setName",param));
cc.addMethod(CtNewMethod.getter("getName",param));

//step 4 :添加无参数的构造函数
CtConstructor constructor = new CtConstructor(new CtClass[]{},cc);
constructor.setBody("{name=\"xiaohei\";}");
cc.addConstructor(constructor);


//step 5:添加有参数构造函数
constructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")},cc);
constructor.setBody("{$0.name=$1;}");
cc.addConstructor(constructor);



CtMethod ctNewMethod = new CtMethod(CtClass.voidType,"printAnimalName",new CtClass[]{},cc);
ctNewMethod.setModifiers(Modifier.PUBLIC);
ctNewMethod.setBody("{System.out.println(name);}");
cc.addMethod(ctNewMethod);
cc.writeFile();

运行完上面的代码会生成如下一个类:

public class AnimalClass {
    private String name = "XiaoMao";

    public void setName(String var1) {
        this.name = var1;
    }

    public String getName() {
        return this.name;
    }

    public AnimalClass() {
        this.name = "xiaohei";
    }

    public AnimalClass(String var1) {
        this.name = var1;
    }

    public void printAnimalName() {
        System.out.println(this.name);
    }
}

ClassPool

是CtClass的对象容器,他的一个作用就是可以读取类文件来构造ctClass,并且保存起来使用,基于HashMap实现,默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。

方法

  1. getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
  2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
  4. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑

CtClass

freeze 冻结一个类,不可修改;isFrozen:判断一个类是否冻结,writeFile:根据ctClass生成.class文件,prune:删除不必要的属性。

修改已经存在的class

public static  void modifyMethod(){

    ClassPool pool = ClassPool.getDefault();

    try{
        pool.appendClassPath("/Users/guosenlin/web/srs/");
        CtClass ct = pool.get("AnimalClass");
        CtMethod m = ct.getDeclaredMethod("printAnimalName");
        m.insertAfter("{System.out.println(\"hshdjshdjshd\");}");
        ct.writeFile();

    }catch (Exception e){
        e.printStackTrace();
    }
}

上面的代码就是在已经存在的类文件AnimalClass里面的printAnimalName方法里插入一段函数:

public void printAnimalName() {
    System.out.println(this.name);
    Object var2 = null;
    System.out.println("hshdjshdjshd");
}

Javassist实现在MainActivity插入代码

自定义Transform

class MyTransform extends Transform{

    private Project project;

    MyTransform(Project project1){
        project = project1
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        for (TransformInput input : transformInvocation.getInputs()) {
            
        }

拿到编译时期的类文件

input.directoryInputs.each {
    DirectoryInput directoryInput  ->
        println ("src.absolutePath:" + directoryInput.file.absolutePath)
        MyInject.inject(directoryInput.file.absolutePath,project)

        def dest = outputProvider.getContentLocation(directoryInput.name,
                directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        //将 input 的目录复制到 output 指定目录
        FileUtils.copyDirectory(directoryInput.file, dest)


}

注入代码

final static inject(String path, Project project){
    println("android.jar: "+project.android.bootClasspath[0])
    /**
     * 将当前路径加入类池
     */
    pool.appendClassPath(path);
    //android.jar
    pool.appendClassPath(project.android.bootClasspath[0].toString());
    pool.importPackage("android.os.Bundle")
    File dir = new File(path)
    if (dir.isDirectory()){

        dir.eachFileRecurse{ file ->
            String filePath = file.absolutePath
            print("filepath: "+ filePath)
            if (file.getName().equals("MainActivity.class")){
                print("filepath: find the MainActivity ")
                //获取Class
                CtClass mainCtClass = pool.get("com.guosen.jassistdemo.MainActivity")
                if (mainCtClass.isFrozen()){
                    mainCtClass.defrost()
                }
                CtMethod method = mainCtClass.getDeclaredMethod("onCreate")
                String insertstr = "{android.widget.Toast.makeText(this,\"这个吐司我是偷偷加进去的\",android.widget.Toast.LENGTH_SHORT).show();}";
                method.insertAfter(insertstr)
                mainCtClass.writeFile(path)
                mainCtClass.detach()
                // android.widget.Toast.makeText(this,"sdsdsd",android.widget.Toast.LENGTH_SHORT).show();
            }

        }
    }
}

注册Transfrom

class MyJavassistPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        print("《应用自定义插件guosen》")
        //注册Transfrom
        project.getExtensions().findByType(AppExtension.class)
        .registerTransform(new MyTransform(project))
    }
}

效果:(运行项目或者执行build)

有些热修复就是采用这种插入代码技术如QQ补丁 www.jianshu.com/p/62503c6db…