Java面向切面编程技术二:javassist

1,661 阅读3分钟

[toc]

运行环境

agp版本 4.1.2 gradle版本 6.5.0 as arctic fox

一.javassist概述

javassist 是 JavaProgrammingAssistant的简称,它本质上是一个java库,用于在不熟悉class字节码结构的情况下,能够修改或者新生成字节码。我们可以通过插入java代码来自动把对应的class字节码插入到class文件中。

相比于apt只能新增java文件,它能够新增或者修改class文件,因此能力更加强大.

二.javassist作用时机

android中javassist一般与transform结合使用,因为我们在javac任务后获得class文件列表,我们需要注入自己写的处理器来进行后续处理

transform作用于class字节码打包成dex的时期,android官方提供了transform任务来方便开发者在class转dex阶段能够插入任务到transfrom pipeline处理class.

通常我们也与javac任务结合起来,比如在javac阶段借助apt记录需要处理的class的信息,然后在transform阶段直接处理class,从而省去遍历class的时间消耗.

三.javassit完整例子

这里例子使用javassit插入代码来统计某些方法的执行时长,使用javassit整体过程包括:

  • 定义注解ExecCost,这个注解标记需要计算耗时的方法.与apt不一样的是 这个定义不是必须,javasssit的使用不依赖于注解.
  • 定义注解处理器CostProcessor,这个注解处理器将所有标记了ExecCost的类记录到一个新生成的类ExecCostCollect.java文件中
  • 定义插件CostPlugin
  • 定义CostTransform注册到plugin中,它会在class文件被转换成 dex 之前执行

3.1 定义插件ExecCost

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ExecCost {
}

注意它的生命周期,虽然我们只需要在source阶段生成java文件,但是在class处理阶段,还需要判断哪些方法需要插入耗时计算代码

3.2 定义CostProcessor

@AutoService(Processor.class)
public class CollectionProcessor extends AbstractProcessor {

    Messager mMessage;
    Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mMessage = processingEnv.getMessager();
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement ann:annotations) {
            Set<? extends Element> sets = roundEnv.getElementsAnnotatedWith(ann);
            TypeSpec spec = buildClass(ann.getSimpleName().toString() , sets);
            writeFile(spec);
        }

        return false;
    }

    private void writeFile(TypeSpec typeSpec){
        JavaFile javaFile = JavaFile.builder("com.hch",typeSpec).build();
        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private TypeSpec buildClass(String annotationName, Set<? extends Element> sets) {
        // public final class name ExecCostCollect
        TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(annotationName+"Collect")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);


        //public String s_onCreate = "com.hch.MyApp";
        int i = 0;
        for (Element ele : sets) {
            Element enclosingElement = ele.getEnclosingElement();
            FieldSpec.Builder builder = FieldSpec.builder(String.class , "s"+i+"_"+ ele.getSimpleName() , Modifier.PUBLIC ,Modifier.FINAL , Modifier.STATIC);
            builder.initializer("$S" , enclosingElement.toString());
            typeSpecBuilder.addField(builder.build());
            i++;
        }

        return typeSpecBuilder.build();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ExecCost.class.getCanonicalName() );
        return types;
    }
}

Processor的定义可以参考第一篇,这里的重点在于process方法,获取用ExecCost标记的类名.

Set<? extends Element> sets = roundEnv.getElementsAnnotatedWith(ann);

buildClass方法根据注解的名字生成对应的collect类,如ExecCost注解生成的类名字为ExecCostCollect.class,同时将标记的类写入. ExecCostCollect.class生成的内容如下:

public final class ExecCostCollect {
    public static final String s0_onCreate = "com.hch.MyApp";
    public static final String s1_onCreate = "com.hch.MainActivity";
    public static final String s2_print = "com.hch.Person";

    public ExecCostCollect() {
    }
}

3.3 定义Costplugin

class CostPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {

        println("CostPlugin start")

        //create extension
        project.getExtensions().create("cost" , CostExtension.class)

//        //register transform
        // AppExtension 必须要在所在的模块依赖  com.android.tools.build:gradle:4.1.2'
        AppExtension appExtension = project.getExtensions().findByType(AppExtension.class);
        appExtension.registerTransform(new CostTransform(project))

        println("CostPlugin end")
    }
}

plugin的定义过程在gradle之插件定义中有详细说明,不再细说.

这里的要注意两点:

  • appExtension.registerTransform(new CostTransform(project)) 将定义的CostTransform注册到tranform pipeline中.
  • AppExtension是在agp中的类,因此我们需要依赖com.android.tools.build:gradle , 也就是在我们定义插件的模块的build.gradle中添加
dependencies {
    // 使用javassist必须依赖
    implementation 'com.android.tools.build:gradle:4.1.2'
    implementation 'org.javassist:javassist:3.24.0-GA'
}

3.4 transform定义

transform的处理过程为

  • 读取apt阶段生成的ExecCostCollect.class中写入的需要处理的class的信息
  • 将需要处理的class ,找到含有ExecCost注解定义的方法,在方法中插入计算耗时的代码
  • 将处理完的代码拷贝到输出中

CostTransform.groovy

public class CostTransform extends Transform {
    ClassPool classPool
    Project project
    public CostTransform(Project p){
        classPool = ClassPool.getDefault()
        project = p
    }

    @Override
    public String getName() {
        return "cost";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        println("transform begin .....");
        super.transform(transformInvocation);

        //project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
        // 提示错误 javassist.CannotCompileException: cannot find android.os.Bundle
        classPool.appendClassPath(project.android.bootClasspath[0].toString());

        def outputProvider = transformInvocation.outputProvider
        for (TransformInput input : transformInvocation.inputs) {

            if (null == input) continue

            //遍历jar文件 对jar不操作,但是要输出到out路径
            for (JarInput jarInput : input.jarInputs) {
                // 重命名输出文件
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }

            //遍历文件夹
            for (DirectoryInput directoryInput : input.directoryInputs) {

                classPool.appendPathList(directoryInput.file.getAbsolutePath())
                // 获取output目录
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

                TraverseDir.traverse(classPool , directoryInput.file.getAbsolutePath() , directoryInput.file , TraverseDir.action_exec)

                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }

        //一定要释放,否则执行多次会复用之前导入的class
        classPool.clearImportedPackages()
        println("transform end .....");
    }
}

3.4.1 读取ExecCostCollect.class 的内容

由于定义的注解都在我们自己的代码中,所以只需要处理文件夹中的类

TraverseDir.traverse(classPool , directoryInput.file.getAbsolutePath() , directoryInput.file , TraverseDir.action_exec)

TraverseDir.groovy

public class TraverseDir {

    public static int action_exec = 0;
    public static int action_nofast = 1

    //存放collect中需要处理的class
    static Map<String,String> map = new HashMap<>();

    static filterAllClasses(ClassPool pool){
        CtClass ctClass = pool.getOrNull("com.hch.ExecCostCollect")
        if (ctClass != null){
            CtField[] fields = ctClass.getDeclaredFields()
            fields.each {
                if (it.getConstantValue() != null){
                    map.put(it.getConstantValue(), false)
                }
            }
        }
    }

    private static String getClassName(String dirPath, String classPath) {
        String packagePath = classPath.substring(dirPath.length() + 1);
        String clazz = packagePath.replaceAll("/", ".");
        String substring = clazz.substring(0, packagePath.length() - ".class".length());
        return substring;
    }

    private static void inject(ClassPool pool , String dirPath, String classPath , String className , int action){
        //需要处理的class
        boolean  needWrite = false;
        CtClass ctClass = pool.getCtClass(className)
        if (map.containsKey(className)){
            // 解冻
            if (ctClass.isFrozen()) {
                ctClass.defrost()
            }

            CtMethod[] ctMethods = ctClass.getDeclaredMethods()
            ctMethods.each {
                //这里显示出用buildSrc的缺陷,它无法依赖到annotation模块,也因此无法直接找到ExecCost的class
                if (action == action_exec){
                    if (it.hasAnnotation("com.hch.annotation.ExecCost")){
                        needWrite = true;
                        println("insert start :${dirPath}/${className}.${it.name}");
                        //只加这一句不会有任何变化
                        it.addLocalVariable("start", CtClass.longType);
                        it.insertBefore("start = System.currentTimeMillis();");
                        String end = "System.out.println(\"" + ctClass.getName() + "." + it.getName() + " exeCost=\" +(System.currentTimeMillis()-start));";
                        it.insertAfter(end);
                        println("insert end !");
                    }
                }else if (action == action_nofast){

                }
            }

            if (needWrite){
                try {
                    //保存class , 会自动补全包名
                    ctClass.writeFile(dirPath);
                    println("writeFile success:${dirPath}");
                } catch (CannotCompileException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            println("detach ctClass");
            ctClass.detach();//释放
        }
    }

    static traverse(ClassPool pool , String dirPath , File file ,int action){
        filterAllClasses(pool)

        File[] fs = file.listFiles();
        for (File f : fs) {
            if (f.isDirectory()) {    //若是目录,则递归
                traverse(pool , dirPath, f , action);
            } else if (f.isFile()) {
                String className = getClassName(dirPath, f.getPath());
                //代码插入
                inject(pool , dirPath , file.getPath(), className , action);
            }
        }
    }
}

filterAllClasses方法根据包名找到com.hch.ExecCostCollect,并且读取它的所有属性的内容. 因为我们写入的内容是以public static final 写入的属性,因此可以直接用getConstantValue获取属性的值.

static filterAllClasses(ClassPool pool){
        CtClass ctClass = pool.getOrNull("com.hch.ExecCostCollect")
        if (ctClass != null){
            CtField[] fields = ctClass.getDeclaredFields()
            fields.each {
                if (it.getConstantValue() != null){
                    map.put(it.getConstantValue(), false)
                }
            }
        }
    }

找到需要处理的类后,遍历所有的类名,对需要处理的类中包含ExecCost注解的方法插入代码

if (map.containsKey(className)){
    // 解冻
    if (ctClass.isFrozen()) {
        ctClass.defrost()
    }

    CtMethod[] ctMethods = ctClass.getDeclaredMethods()
    ctMethods.each {
        //这里显示出用buildSrc的缺陷,它无法依赖到annotation模块,也因此无法直接找到ExecCost的class
        if (action == action_exec){
            if (it.hasAnnotation("com.hch.annotation.ExecCost")){
                ...
            }
        }else if (action == action_nofast){

        }
    }
    ....
}

3.4.2 写入计算耗时代码

if (action == action_exec){
    if (it.hasAnnotation("com.hch.annotation.ExecCost")){
        needWrite = true;
        println("insert start :${dirPath}/${className}.${it.name}");
        //只加这一句不会有任何变化
        it.addLocalVariable("start", CtClass.longType);
        it.insertBefore("start = System.currentTimeMillis();");
        String end = "System.out.println(\"" + ctClass.getName() + "." + it.getName() + " exeCost=\" +(System.currentTimeMillis()-start));";
        it.insertAfter(end);
        println("insert end !");
    }
}

写入的时候,要注意insertBefore需要依赖android.jar包,如果不加入,会提示错误 javassist.CannotCompileException: cannot find android.os.Bundle

我们提前在transform方法中加入了android.jar的依赖

classPool.appendClassPath(project.android.bootClasspath[0].toString());

3.4.3 将修改过的文件写到输出

在transform中,我们处理完class之后,将所有的class都写到outpurProvider提供的输出路径中,jar也需要拷贝.

for (JarInput jarInput : input.jarInputs) {
    // 重命名输出文件
    def jarName = jarInput.name
    def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
    if (jarName.endsWith(".jar")) {
        jarName = jarName.substring(0, jarName.length() - 4)
    }
    def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
    FileUtils.copyFile(jarInput.file, dest)
}

for (DirectoryInput directoryInput : input.directoryInputs) {
    classPool.appendPathList(directoryInput.file.getAbsolutePath())
    // 获取output目录
    def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

    TraverseDir.traverse(classPool , directoryInput.file.getAbsolutePath() , directoryInput.file , TraverseDir.action_exec)

    // 将input的目录复制到output指定目录
    FileUtils.copyDirectory(directoryInput.file, dest)
}

3.5 踩到的坑记录

3.5.1 ExecCost注解的生命周期定义为RetentionPolicy.source导致class注解阶段找不到注解标记的方法

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ExecCost {
}

3.5.2 找不到AppExtension

AppExtension在agp中,需要在transform所在模块增加依赖

dependencies {
    // 使用javassist必须依赖
    implementation 'com.android.tools.build:gradle:4.1.2'
    implementation 'org.javassist:javassist:3.24.0-GA'
}

3.5.3 javassist 执行insertBefore时,提示错误 javassist.CannotCompileException: cannot find android.os.Bundle

这是因为classpool在模拟jvm的逻辑,在activity中插入代码需要能够模拟android环境,因此需要依赖android.jar

classPool.appendClassPath(project.android.bootClasspath[0].toString());

3.5.4 ctField.getDeclaredFields 读取class中的常量返回null

原本生成ExecCostCollect.class时,创建的属性为public static s_xxxx = "com.hch.xxx" ,这样读出来的值为null,

需要修改为public static final s_xxxx = "com.hch.xxx"

FieldSpec.Builder builder = FieldSpec.builder(String.class , "s"+i+"_"+ ele.getSimpleName() , Modifier.PUBLIC ,Modifier.FINAL , Modifier.STATIC);