360 Argus APM 源码分析(2)—— argus-apm-gradle源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/foolish0421/article/details/86633030

argus-apm-gradle源码分析

argus-apm-gradle工程定义了一个gradle plugin,主要有以下两个作用:

  1. 支持AOP编程,方便ArgusAPM能够在编译期织入一些性能采集的代码;
  2. 通过Gradle插件来管理依赖库,使用户接入ArgusAPM更简单。

argus-apm-gradle使用kotlin语言开发。这里我们假定大家已经熟悉gradle plugin的开发。
resources/META-INF/gradle-plugins/目录下的argusapm.properties指明了plugin的入口:

implementation-class=com.argusapm.gradle.AspectJPlugin

这是个java properties文件,指明了plugin的入口是com.argusapm.gradle.AspectJPlugin。
我们看一下com.argusapm.gradle.AspectJPlugin类的代码。

internal class AspectJPlugin : Plugin<Project> {
    private lateinit var mProject: Project
    override fun apply(project: Project) {
        mProject = project
        project.extensions.create(AppConstant.USER_CONFIG, ArgusApmConfig::class.java)

        //公共配置初始化,方便获取公共信息
        PluginConfig.init(project)

        //自定义依赖库管理
        project.gradle.addListener(ArgusDependencyResolutionListener(project))

        project.repositories.mavenCentral()
        project.compatCompile("org.aspectj:aspectjrt:1.8.9")


        if (project.plugins.hasPlugin(AppPlugin::class.java)) {
            project.gradle.addListener(BuildTimeListener())

            val android = project.extensions.getByType(AppExtension::class.java)
            android.registerTransform(AspectJTransform(project))
        }
    }
}

类AspectJPlugin实现接口org.gralde.api.Plugin,有一个org.gradle.api.Project的范型参数。接口Plugin只有一个方法:apply(T var1)。AspectJPlugin中定义了一个mProject属性,并且在apply方法中使用类型参数project进行初始化。project.extensions.create(AppConstant.USER_CONFIG, ArgusApmConfig::class.java)接收ArgusApmConfig中定义的参数,而create的第一个参数,是在使用插件的项目的build.gradle 文件中的进行参数配置的dsl的名字。接下来调用PluginConfig.init(project)进行初始化。project.gradle.addListener(ArgusDependencyResolutionListener(project))实现了自定义依赖库管理。project.repositories.mavenCentral()将中央仓库指向默认的maven central。project.compatCompile("org.aspectj:aspectjrt:1.8.9")添加aspectjrt依赖。compatCompile是通过kotlin的扩展功能为Project临时扩展的函数。ArgusDependencyResolutionListener类实现了DependencyResolutionListener接口,用来监听构建过程中依赖的关系。最后,如果使用plugin的工程中有AppPlugin(apply plugin: 'com.android.application'),  再添加BuildTimeListener,该类实现了TaskExecutionListener, BuildListener两个接口,监控build的时间。然后,获取Extension的具体类型,赋值给android,并为android注册AspectJTransform。

AspectJPlugin类详细分析

我们再回到AspectJPlugin类,详细的分析一下apply方法的流程,以及涉及的类和方法。
PluginConfig类init的代码如下所示:

fun init(project: Project) {
    val hasAppPlugin = project.plugins.hasPlugin(AppPlugin::class.java)
    val hasLibPlugin = project.plugins.hasPlugin(LibraryPlugin::class.java)
    if (!hasAppPlugin && !hasLibPlugin) {
        throw  GradleException("argusapm: The 'com.android.application' or 'com.android.library' plugin is required.")
    }

    Companion.project = project

    getVariants(project).all { variant ->
        val javaCompile = variant.javaCompile as JavaCompile
        encoding = javaCompile.options.encoding
        bootClassPath = getBootClasspath().joinToString(File.pathSeparator)
        sourceCompatibility = javaCompile.sourceCompatibility
        targetCompatibility = javaCompile.targetCompatibility
    }
}

init方法一次调用project.plugins.hasPlugin(AppPlugin::class.java)和project.plugins.hasPlugin(LibraryPlugin::class.java)来判断project是否有app plugin和lib plugin。如果两种都不存在,就会抛出GradleException。 接下来把参数project赋值给类的Companion变量project。获取project的所有build variant,对所有variants执行以下操作:获取variant的javaCompile,用javaCompile中的encoding,bootClassPath,sourceCompatibility和targetCompatibility,对类的对应属性进行初始化。其中,bootClassPath要通过调用getBootClasspath().joinToString(File.pathSeparator)获取。getBootClasspath的代码如下:

private fun getBootClasspath(): List<File> {
    val hasAppPlugin = project.plugins.hasPlugin(AppPlugin::class.java)
    val plugin = project.plugins.getPlugin(if (hasAppPlugin) {
        AppPlugin::class.java
    } else {
        LibraryPlugin::class.java
    })
    val extAndroid = if (hasAppPlugin) {
        project.extensions.getByType(AppExtension::class.java)
    } else {
        project.extensions.getByType(LibraryExtension::class.java)
    }
    return extAndroid.bootClasspath
            ?: plugin::class.java.getMethod("getRuntimeJarList").invoke(plugin) as List<File>
}

首先判断project是否有app plugin,如果存在,则通过project.plugins.getPlugin获取一个AppPlugin,否则,获取一个LibraryPlugin。如果存在app plugin,则从project extensions(ExtensiionContainer)中,获取AppExtensioin,赋值给变量extAndroid;否则,获取LibraryExtension,赋值给extAndroid。如果extAndroidgetBootClasspath()返回列表非空,就返回这个列表;否则,通过反射,找到plugin(Plugin类型)的getRuntimeJarList方法,调用该方法,并且把该方法的返回转换为List作为getBootClasspat()的返回。
回到AspectJPlugin的apply方法, project.gradle.addListener(ArgusDependencyResolutionListener(project))添加自定义依赖哭管理,这对应着使用argus-apm-gradle插件的项目中的argusapm.gradle文件中的moduleDependencies。ArgusDependencyResolutionListener实现了DependencyResolutionListener接口,重写了beforeResolve方法。该方法主要是决定使用网络上的lib还是本地的module。

##AspectJTransform类
AspectJTransform类继承自com.android.build.api.transform.Transform,主要的逻辑都在override的方法transform中:

override fun transform(transformInvocation: TransformInvocation) {
    val transformTask = transformInvocation.context as TransformTask
    LogStatus.logStart(transformTask.variantName)

    //第一步:对输入源Class文件进行切割分组
    val fileFilter = FileFilter(project, transformTask.variantName)
    val inputSourceFileStatus = InputSourceFileStatus()
    InputSourceCutter(transformInvocation, fileFilter, inputSourceFileStatus).startCut()

    //第二步:如果含有AspectJ文件,则开启织入;否则,将输入源输出到目标目录下
    if (PluginConfig.argusApmConfig().enabled && fileFilter.hasAspectJFile()) {
        AjcWeaverManager(transformInvocation, inputSourceFileStatus).weaver()
    } else {
        outputFiles(transformInvocation)
    }

    LogStatus.logEnd(transformTask.variantName)
}

transform方法第一步

transform方法首先定义一个TransformTask类型的变量transformTask,并且用参数transformInvocation的context属性对它进行初始化。然后开始输出log。
接下来,对输入源class文件进行切割分组。我们分析FileFilter的源代码。
FileFilter构造函数有两个参数:private val project: Project, private val variantName: String。前一个参数是Project信息,后一个参数,指明编译的变体,即debug或release等。FileFilter的init代码块如下:

init {
    init()
}

private fun init() {
    basePath = project.buildDir.absolutePath + File.separator + AndroidProject.FD_INTERMEDIATES + "/${AppConstant.TRANSFORM_NAME}/" + variantName
    aspectPath = basePath + File.separator + "aspects"
    includeDirPath = basePath + File.separator + "include_dir"
    excludeDirPath = basePath + File.separator + "exclude_dir"
}

这里实际上指明了gradle编译过程中各个路径,basePath就位于我们build中常见的imtermediates目录下,它可能是这样的:{project root dir}/{module dir}/build/imtermediates/argus_apm_ajx/debug/。后面的几个path都位于这个路径下。

回到AspectJTransform::transform()中的代码,定义并且初始化了inputSourceFileStatus之后,实例化InputSourceCutter类并且调用它的startCut()方法:InputSourceCutter(transformInvocation, fileFilter, inputSourceFileStatus).startCut()
下面是一个目录的实际的例子:
Argus Build Path

扫描二维码关注公众号,回复: 5268835 查看本文章

##InputSourceCutter类
类InputSourceCutter的构造函数有三个参数, 分别是transformInvocation,前边定义的fileFilter和inputSourceFileStatus,它的init方法如下:

init {
    if (transformInvocation.isIncremental) {
        LogStatus.isIncremental("true")
        LogStatus.cutStart()

        transformInvocation.inputs.forEach { input ->
            input.directoryInputs.forEach { dirInput ->
                whenDirInputsChanged(dirInput)
            }

            input.jarInputs.forEach { jarInput ->
                whenJarInputsChanged(jarInput)
            }
        }

        LogStatus.cutEnd()
    } else {
        LogStatus.isIncremental("false")
        LogStatus.cutStart()

        transformInvocation.outputProvider.deleteAll()

        transformInvocation.inputs.forEach { input ->
            input.directoryInputs.forEach { dirInput ->
                cutDirInputs(dirInput)
            }

            input.jarInputs.forEach { jarInput ->
                cutJarInputs(jarInput)
            }
        }
        LogStatus.cutEnd()
    }
}

init方法中,先判断transformInvocation是不是incremental的,如果是,就遍历transformInvocation中的input列表,接着遍历该列表下每一个input的directoryInputs和jarInputs列表,分别调用whenDirInputsChanged和whenJarInputsChanged方法。如果transformInvocation不是incremental的,就删除transformInvocation的outputProvider中的所有内容。然后遍历transformInvocation中的input列表,接着遍历该列表下每一个input的directoryInputs和jarInputs列表,分表调用cutDirInputs和cutJarInputs。
InputSourceCutter中定义了一个ThreadPool类型的属性taskManager,ThreadPool类维护了一个定长线程池。
首先我们看whenDirInputsChanged的代码

private fun whenDirInputsChanged(dirInput: DirectoryInput) {
    taskManager.addTask(object : ITask {
        override fun call(): Any? {
            dirInput.changedFiles.forEach { (file, status) ->
                fileFilter.whenAJClassChangedOfDir(dirInput, file, status, inputSourceFileStatus)
                fileFilter.whenClassChangedOfDir(dirInput, file, status, inputSourceFileStatus)
            }

            //如果include files 发生变化,则删除include输出jar
            if (inputSourceFileStatus.isIncludeFileChanged) {
                logCore("whenDirInputsChanged include")
                val includeOutputJar = transformInvocation.outputProvider.getContentLocation("include", contentTypes as Set<QualifiedContent.ContentType>, scopes, Format.JAR)
                FileUtils.deleteQuietly(includeOutputJar)
            }

            //如果exclude files发生变化,则重新生成exclude jar到输出目录
            if (inputSourceFileStatus.isExcludeFileChanged) {
                logCore("whenDirInputsChanged exclude")
                val excludeOutputJar = transformInvocation.outputProvider.getContentLocation("exclude", contentTypes as Set<QualifiedContent.ContentType>?, scopes, Format.JAR)
                FileUtils.deleteQuietly(excludeOutputJar)
                mergeJar(getExcludeFileDir(), excludeOutputJar)
            }
            return null
        }
    })
}

whenDirInputsChanged往taskManager中添加一个新的task。这个task的工作内容如下:遍历whenDirInputsChanged的参数dirInput目录中发生变化的文件,然后调用fileFilter的whenAJClassChangedOfDir方法和whenClassChangedOfDir方法。如果属性inputSourceFileStatus中的isIncludeFileChanged变成true,即include files 发生变化,则删除include输出jar;如果inputSourceFileStatus的isExcludeFileChanged为true,即exclude files发生变化,则重新生成exclude jar到输出目录。最后,调用mergeJar生成新的jar文件。
whenJarInputsChanged的file如下:

private fun whenJarInputsChanged(jarInput: JarInput) {
    if (jarInput.status != Status.NOTCHANGED) {
        taskManager.addTask(object : ITask {
            override fun call(): Any? {
                fileFilter.whenAJClassChangedOfJar(jarInput, inputSourceFileStatus)
                fileFilter.whenClassChangedOfJar(transformInvocation, jarInput)
                return null
            }
        })
    }
}

如果参数jarInput的status不等于Status.NOTCHANGED,就是jarInput表示的jar文件发生了变化,whenJarInputsChanged向taskManager中添加一个新的task。这个task依次调用fileFilterwhenAJClassChangedOfJar方法和whenClassChangedOfJar方法。
cutDirInputs方法的代码如下:

private fun cutDirInputs(dirInput: DirectoryInput) {
    taskManager.addTask(object : ITask {
        override fun call(): Any? {
            dirInput.file.eachFileRecurse { file ->
                //过滤出AJ文件
                fileFilter.filterAJClassFromDir(dirInput, file)
                //过滤出class文件
                fileFilter.filterClassFromDir(dirInput, file)
            }

            //put exclude files into jar
            if (countOfFiles(getExcludeFileDir()) > 0) {
                val excludeJar = transformInvocation.outputProvider.getContentLocation("exclude", contentTypes as Set<QualifiedContent.ContentType>, scopes, Format.JAR)
                mergeJar(getExcludeFileDir(), excludeJar)
            }
            return null
        }
    })
}

cutDirInputs也向taskManager中添加一个task。这个task的工作内容是:遍历参数dirInput中的每一个文件,调用fileFilter的filterAJClassFromDir和filterClassFromDir。如果exclude File Dir中的文件数大于0,就调用mergeJar,把这些exclude files放入jar文件。
方法cutJarInputs的代码如下:

private fun cutJarInputs(jarInput: JarInput) {
    taskManager.addTask(object : ITask {
        override fun call(): Any? {
            fileFilter.filterAJClassFromJar(jarInput)
            fileFilter.filterClassFromJar(transformInvocation, jarInput)
            return null
        }
    })
}

cutJarInputs向taskManager中添加一个task。task调用fileFilter的filterAJClassFromJar和filterClassFromJar方法,从jar文件中过滤掉AJ文件和class文件。
最后,我们回到AspectJTransform的transform方法,它调用了InputSourceCutter的starCut方法,这个方法调用了taskManager.startWork(),开启taskManager中的所有task。taskManager中的task,是通过前面分析的几个方法加入的,最初的入口是InputSourceCutter的init方法。

FileFilter源码

下面我们来分析FileFilter的源码。
构造函数和init方法我们之前已经分析过了。我们先来看看前边在InputSourceCutter中用到的几个方法。
先来看whenAJClassChangedOfDir方法。

fun whenAJClassChangedOfDir(dirInput: DirectoryInput, file: File, status: Status, inputSourceFileStatus: InputSourceFileStatus) {
    if (isAspectClassFile(file)) {
        log("aj class changed ${file.absolutePath}")
        inputSourceFileStatus.isAspectChanged = true
        val path = file.absolutePath
        val subPath = path.substring(dirInput.file.absolutePath.length)
        val cacheFile = File(aspectPath + subPath)

        when (status) {
            Status.REMOVED -> {
                FileUtils.deleteQuietly(cacheFile)
            }
            Status.CHANGED -> {
                FileUtils.deleteQuietly(cacheFile)
                cache(file, cacheFile)
            }
            Status.ADDED -> {
                cache(file, cacheFile)
            }
            else -> {
            }
        }
    }
}

whenAJClassChangedOfDir首先调用isAspectClassFile(file)这是不是一个aspect class文件。isAspectClassFile(file)首先判断文件是不是一个classfile,进一步通过isAspectClass(FileUtils.readFileToByteArray(file))读入文件内容并转换为byte array,判断这是不是一个aspect class文件。isAspectClass方法位于Utils类,其代码如下:

fun isAspectClass(bytes: ByteArray): Boolean {
    if (bytes.isEmpty()) {
        return false
    }

    try {
        val classReader = ClassReader(bytes)
        val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES)
        val aspectJClassVisitor = AspectJClassVisitor(classWriter)
        classReader.accept(aspectJClassVisitor, ClassReader.EXPAND_FRAMES)
        return aspectJClassVisitor.isAspectClass
    } catch (e: Exception) {

    }

    return false
}

isAspectClass用到了ASM框架。ASM是一个Java字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ClassReader,ClassWriter类都来自该框架。
ClassReader类可以直接由字节数组或由class文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用accept方法,这个方法接受一个实现了ClassVisitor接口的对象实例作为参数,然后依次调用 ClassVisitor接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,ClassReader知道如何调用各种visit函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的Visitor来对字节码树进行不同的修改。ClassVisitor会产生一些子过程,比如visitMethod会返回一个实现MethordVisitor接口的实例,visitField会返回一个实现 FieldVisitor接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于ClassReader来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过ClassReader类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。
ClassWriter实现了ClassVisitor接口,而且含有一个toByteArray()函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有visit事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后)。
AspectJClassVisitor扩展自ClassVisitor类,并且重写了visitAnnotation方法.visitAnnotation方法声明:public AnnotationVisitor visitAnnotation(String desc, boolean visible)。参数desc表示被注解修饰的class的描述符,参数visible表示注解在运行时是否可见.重写的visitAnnotation会判断类前有没有“org/aspectj/lang/annotation/Aspect”注解。这也是判断是否是Aspect class的依据。
如果类前包含以上注解,那么它就是一个Aspect class,那么回到FiltFilter的isAspectClassFile方法。如果这是一个Aspect class,那么我们先要把inputSourceFileStatus.isAspectChanged设置成true,标记Aspect class 发生了改变,并且设置path和subPath,subPathdirInput的路径除去所有前导目录,cacheFile的路径就是aspectPath再加上subPath。接下来,根据变化的status,采取进一步操作。如果是Status.REMOVED,就删除cacheFile;如果是Status.CHANGED,首先删除cacheFile,然后调用cache(file, cacheFile)方法,生成新的cache文件;如果状态是Status.ADDED,就直接调用cache(file, cacheFile)生成新的cache文件;其它状态不处理。
接下来我们看whenClassChangedOfDir的代码:

fun whenClassChangedOfDir(dirInput: DirectoryInput, file: File, status: Status, inputSourceFileStatus: InputSourceFileStatus) {
	val path = file.absolutePath
    val subPath = path.substring(dirInput.file.absolutePath.length)
    val transPath = subPath.replace(File.separator, ".")

    val isInclude = isIncludeFilterMatched(transPath, PluginConfig.argusApmConfig().includes) && !isExcludeFilterMatched(transPath, PluginConfig.argusApmConfig().excludes)

    if (!inputSourceFileStatus.isIncludeFileChanged && isInclude) {
        inputSourceFileStatus.isIncludeFileChanged = isInclude
    }

    if (!inputSourceFileStatus.isExcludeFileChanged && !isInclude) {
        inputSourceFileStatus.isExcludeFileChanged = !isInclude
    }

    val target = File(if (isInclude) {
        includeDirPath + subPath
    } else {
        excludeDirPath + subPath
    })
    when (status) {
        Status.REMOVED -> {
            logCore("[ Status.REMOVED ] file path is ${file.absolutePath}")
            FileUtils.deleteQuietly(target)
        }
        Status.CHANGED -> {
            logCore("[ Status.CHANGED ] file path is ${file.absolutePath}")
            FileUtils.deleteQuietly(target)
            cache(file, target)
        }
        Status.ADDED -> {
            logCore("[ Status.ADDED ] file path is ${file.absolutePath}")
            cache(file, target)
        }
        else -> {
            logCore("else file path is ${file.absolutePath}")
        }
    }
}

whenClassChangedOfDir和whenAJClassChangedOfDir类似。首先获得transform path,和aspect path不同,transform path,就是传入file的绝对路径的父目录。接下来调用isIncludeFilterMatched方法和isExcludeFilterMatched,判断include路径和exclude路径是否匹配。如果匹配include路径并且不匹配exclude路径,并且inputSourceFileStatus.isIncludeFileChanged原来的值为false,就把inputSourceFileStatus.isIncludeFileChanged标记为true;如果匹配exlcue路径,并且inputSourceFileStatus.isExcludeFileChanged与阿里来的值为false,那么就把inputSourceFileStatus.isExcludeFileChanged标记为true。接下来生成一个target文件,根据之前isInclude的值决定生成include还是exclude文件。最后,根据参数status的值,对target文件执行不同的操作。如果是Status.REMOVED,就删除目标文件;如果是Status.CHANGED,就先删除目标文件,然后以参数file的内容,重新生成一个target文件;如果是Status.ADDED,就直接以参数file的内容,生成一个新的目标文件。
接下来,我们来分析whenAJClassChangedOfJar函数。

fun whenAJClassChangedOfJar(jarInput: JarInput, inputSourceFileStatus: InputSourceFileStatus) {
    val jarFile = java.util.jar.JarFile(jarInput.file)
    val entries = jarFile.entries()
    while (entries.hasMoreElements()) {
        val jarEntry = entries.nextElement()
        val entryName = jarEntry.name
        if (!jarEntry.isDirectory && isClassFile(entryName)) {
            val bytes = ByteStreams.toByteArray(jarFile.getInputStream(jarEntry))
            val cacheFile = java.io.File(aspectPath + java.io.File.separator + entryName)
            if (isAspectClass(bytes)) {
                inputSourceFileStatus.isAspectChanged = true
                when {
                    jarInput.status == Status.REMOVED -> FileUtils.deleteQuietly(cacheFile)
                    jarInput.status == Status.CHANGED -> {
                        FileUtils.deleteQuietly(cacheFile)
                        cache(bytes, cacheFile)
                    }
                    jarInput.status == Status.ADDED -> {
                        cache(bytes, cacheFile)
                    }
                }
            }
        }
        jarFile.close()
    }
}

whenAJClassChangedOfJar获取输入的jar文件,并且解析出jar中的条目。然后遍历这个条目列表。如果该条目不是目录,并且是class文件,就读取该class文件的内容,并且转换为byte array,输出到变量bytes。生成一个cache文件,文件路径是aspect目录的路径+当前这个条目对应的class文件的名字。如果这是一个aspect文件,就把inputSourceFileStatus.isAspectChanged标记为true。然后根据参数jarInput中status的值,对cache文件进行不同的处理:如果status是Status.REMOVED,就删除缓存文件;如果是Status.CHANGED,就先删除cache文件,在用bytes的内容重新生成一个cache文件;如果是Status.ADDED,就直接以bytes中的内容生成一个新的cache文件。最后,记得把jar文件关闭。
接下来,我们来分析whenClassChangedOfJar函数。

fun whenClassChangedOfJar(transformInvocation: TransformInvocation, jarInput: JarInput) {
    val outputJar = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)

    when {
        jarInput.status == Status.REMOVED -> {
            FileUtils.deleteQuietly(outputJar)
        }
        jarInput.status == Status.ADDED -> {
            filterJar(jarInput, PluginConfig.argusApmConfig().includes, PluginConfig.argusApmConfig().excludes, PluginConfig.argusApmConfig().excludeJars)
        }
        jarInput.status == Status.CHANGED -> {
            FileUtils.deleteQuietly(outputJar)
        }
    }

    //将不需要做AOP处理的文件原样copy到输出目录
    if (!filterJar(jarInput, PluginConfig.argusApmConfig().includes, PluginConfig.argusApmConfig().excludes, PluginConfig.argusApmConfig().excludeJars)) {
        FileUtils.copyFile(jarInput.file, outputJar)
    }
}

首先,通过transformInvocation.outputProvider.getContentLocation获取输出jar文件outputJar。根据参数jarInput中status的值,决定对ouputJar的动作。如果是Status.REMOVED,就删除outputJar;如果是Status.ADDED,就调用filterJar函数,生成一个新的outputJar文件;如果是Status.CHANGE,就把outputJar直接删掉。对于不需要做AOP处理的文件,调用FileUtils.copyFile(jarInput.file, outputJar),原样拷贝到outputJar。
方法filterJar位于Utils类中,它的作用就是参数includes或excludes以及excludeJars中有没有和jarInput匹配的路径。代码比较简单,这里省略。

下面,我们分析在InputSourceCutter类中调用的FileFilter的以下几个方法:filterAJClassFromDir,filterClassFromDir,filterAJClassFromJar,filterClassFromJar。
filterAJClassFromDir的代码如下:

fun filterAJClassFromDir(dirInput: DirectoryInput, file: File) {
    if (isAspectClassFile(file)) {
        val path = file.absolutePath
        val subPath = path.substring(dirInput.file.absolutePath.length)
        val cacheFile = File(aspectPath + subPath)
        cache(file, cacheFile)
    }
}

这个函数首先判断参数file是不是aspect class文件,如果是,就用aspect目录+file的文件名组成一个缓存文件路径cacheFile,调用cache方法,将file文件中的内容,输出到cacheFile中。
filterClassFromDir的代码如下:

fun filterClassFromDir(dirInput: DirectoryInput, file: File) {
    if (isClassFile(file)) {
        val path = file.absolutePath
        val subPath = path.substring(dirInput.file.absolutePath.length)
        val transPath = subPath.replace(File.separator, ".")

        val isInclude = isIncludeFilterMatched(transPath, PluginConfig.argusApmConfig().includes) &&
                !isExcludeFilterMatched(transPath, PluginConfig.argusApmConfig().excludes)
        if (isInclude) {
            cache(file, File(includeDirPath + subPath))
        } else {
            cache(file, File(excludeDirPath + subPath))
        }
    }
}

首先判断参数file是不是一个class文件,如果是,就从file的绝对路径换算出transPath,然后,判断transPath是不是匹配indluces路径或者excludes路径。如果匹配includes路径,就调用cache方法,把file中的内容,输出到一个新的文件,文件路径是include路径+上文计算出的subPath;如果匹配excludes路径,就调用cache方法,把file中的内容,输出到一个新的文件,文件路径是exclude路径+上文计算出的subPath。
方法filterAJClassFromJar的代码如下:

fun filterAJClassFromJar(jarInput: JarInput) {
    val jarFile = JarFile(jarInput.file)
    val entries = jarFile.entries()
    while (entries.hasMoreElements()) {
        val jarEntry = entries.nextElement()
        val entryName = jarEntry.name
        if (!(jarEntry.isDirectory || !isClassFile(entryName))) {
            val bytes = ByteStreams.toByteArray(jarFile.getInputStream(jarEntry))
            val cacheFile = File(aspectPath + File.separator + entryName)
            if (isAspectClass(bytes)) {
                cache(bytes, cacheFile)
            }
        }
    }

    jarFile.close()

}

从参数jarInput中解析出jar文件,并且遍历jar文件中的条目,如果该条目不是目录,并且不是class文件,读取文件的内容到Byte Array类型的变量bytes中,用aspect目录+当前条目的名称组成一个路径,并以该路径生成一个cache文件,如果从获取的bytes判断出这是一个aspect文件,就调用cache方法把bytes的内容输出到cacheFile。最后,把jarFile关闭。
filterClassFromJar的代码如下:

fun filterClassFromJar(transformInvocation: TransformInvocation, jarInput: JarInput) {
    //如果该Jar包不需要参与到AJC代码织入的话,则直接拷贝到目标文件目录下
    if (!filterJar(jarInput, PluginConfig.argusApmConfig().includes, PluginConfig.argusApmConfig().excludes, PluginConfig.argusApmConfig().excludeJars)) {
        val dest = transformInvocation.outputProvider.getContentLocation(jarInput.name
                , jarInput.contentTypes
                , jarInput.scopes
                , Format.JAR)
        FileUtils.copyFile(jarInput.file, dest)
    }
}

首先通过filterJar判断参数jarInput是否匹配includes,excludes或者excludeJars路径。如果都不匹配,说明Jar包不需要参与到AJC代码织入,直接把该jar文件拷贝到目标文件目录下即可。
以上一组函数名中都包含“filter”,在执行目标操作之前都要进行以下“filter”操作。

到目前为止,我们实际上分析完了AspectJTransform类中transform方法的第一步。接下来我们分析该方法的第二步。

transform方法第二步

首先判断PluginConfig.argusApmConfig().enabled是否为true,即argus apm有没有使能,同时判断第一步生成的fileFilter是否包含aspect 文件。如果两个条件都满足,就用transformInvocation和inputSourceFileStatus生成一个AjcWeaverManager的对象,并且调用该对象的weaver方法。如果前边的两个条件有一个不满足,调用outputFiles(transformInvocation)输出文件的内容。这个方法的具体内容我们稍后分析。首先我们先先来看看类AjcWeaverManager的代码。
类AjcWeaverManager的默认构造函数有两个参数,TransformInvocation类型和InputSourceFileStatus类型,这两个参数会初始化类的属性transformInvocation和inputSourceFileStatus。类中还有以下几个属性:

private val threadPool = ThreadPool()
private val aspectPath = arrayListOf<File>()
private val classPath = arrayListOf<File>()

threadPool定义了一个线程池,aspectPath和classPath分别用来存储aspect文件路径和class文件路径的列表。
weaver函数的代码如下:

fun weaver() {

    System.setProperty("aspectj.multithreaded", "true")

    if (transformInvocation.isIncremental) {
        createIncrementalTask()
    } else {
        createTask()
    }
    log("AjcWeaverList.size is ${threadPool.taskList.size}")

    aspectPath.add(getAspectDir())
    classPath.add(getIncludeFileDir())
    classPath.add(getExcludeFileDir())

    threadPool.taskList.forEach { ajcWeaver ->
        ajcWeaver as AjcWeaver
        ajcWeaver.encoding = PluginConfig.encoding
        ajcWeaver.aspectPath = aspectPath
        ajcWeaver.classPath = classPath
        ajcWeaver.targetCompatibility = PluginConfig.targetCompatibility
        ajcWeaver.sourceCompatibility = PluginConfig.sourceCompatibility
        ajcWeaver.bootClassPath = PluginConfig.bootClassPath
        ajcWeaver.ajcArgs = PluginConfig.argusApmConfig().ajcArgs
    }
    threadPool.startWork()
}

首先,设置系统属性aspectj.multithreaded为true,允许多线程运行aspectj。如果transformInvocation.isIncremental为true,也就是支持增量编译,那么调用方法createIncrementalTask,创建增量编译任务;否则,调用方法createTask,创建普通(全量)编译任务。接下来,遍历threadPool的taskList,taskList中的条目通过上文的createIncrementalTask或者createTask添加,这些条目的类型是AjcWeaver。设置每一个条目的encoding,aspectPath,classPath,targetCompatibility,sourceCompatibility,bootClassPath(启动类),ajcArgs(aspectj的参数)。设置好这些参数之后,调用threadPool.startWork(),启动线程执行。
我们先来看看上文提到的两个create方法。先来看一下createTask:

private fun createTask() {

    val ajcWeaver = AjcWeaver()
    val includeJar = transformInvocation.outputProvider.getContentLocation("include", contentTypes as Set<QualifiedContent.ContentType>, scopes, Format.JAR)
    if (!includeJar.parentFile.exists()) {
        FileUtils.forceMkdir(includeJar.parentFile)
    }
    FileUtils.deleteQuietly(includeJar)
    ajcWeaver.outputJar = includeJar.absolutePath
    ajcWeaver.inPath.add(getIncludeFileDir())
    addAjcWeaver(ajcWeaver)

    transformInvocation.inputs.forEach { input ->
        input.jarInputs.forEach { jarInput ->
            classPath.add(jarInput.file)
            //如果该Jar参与AJC织入的话,则进行下面操作
            if (filterJar(jarInput, PluginConfig.argusApmConfig().includes, PluginConfig.argusApmConfig().excludes, PluginConfig.argusApmConfig().excludeJars)) {
                val tempAjcWeaver = AjcWeaver()
                tempAjcWeaver.inPath.add(jarInput.file)

                val outputJar = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes,
                        jarInput.scopes, Format.JAR)
                if (!outputJar.parentFile?.exists()!!) {
                    outputJar.parentFile?.mkdirs()
                }

                tempAjcWeaver.outputJar = outputJar.absolutePath
                addAjcWeaver(tempAjcWeaver)
            }

        }
    }

}

首先,创建一个AjcWeaver的对象ajcWeaver。获取include jar文件includeJar,如果includeJar的父目录不存在,就创建它的父目录。删除includeJar。ajcWeaver的outputJar设置成includeJar的absolutePath,将Include file目录添加到ajcWeaver的inPath列表。调用addAjcWeaver(ajcWeaver),实际上把ajcWeaver添加到threadPool的task列表。遍历参数transformInvocation的inputs列表,对每一个item,遍历它的jarInputs列表,把每一个jar文件添加到classPath列表。然后调用filterJar方法,判断该jar文件是否要参与AJC织入。如果要参与,首先生成一个AjcWeaver的对象tempAjcWeaver,获取output jar文件,如果outputJar文件的父目录不存在,首先创建该目录。把tempAjcWeaver的outputJar设成outputJar的绝对路径,然后调用addAjcWeaver把tempAjcWeaver添加到threadPool的taskList。
createIncrementalTask的代码如下:

private fun createIncrementalTask() {
    //如果AJ或者Include文件有一个变化的话,则重新织入
    if (inputSourceFileStatus.isAspectChanged || inputSourceFileStatus.isIncludeFileChanged) {
        val ajcWeaver = AjcWeaver()
        val outputJar = transformInvocation.outputProvider?.getContentLocation("include", contentTypes as Set<QualifiedContent.ContentType>, scopes, Format.JAR)
        FileUtils.deleteQuietly(outputJar)

        ajcWeaver.outputJar = outputJar?.absolutePath
        ajcWeaver.inPath.add(getIncludeFileDir())
        addAjcWeaver(ajcWeaver)

        logCore("createIncrementalTask isAspectChanged: [ ${inputSourceFileStatus.isAspectChanged} ]    isIncludeFileChanged:  [ ${inputSourceFileStatus.isIncludeFileChanged} ]")
    }


    transformInvocation.inputs?.forEach { input ->
        input.jarInputs.forEach { jarInput ->
            classPath.add(jarInput.file)
            val outputJar = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)

            if (!outputJar.parentFile?.exists()!!) {
                outputJar.parentFile?.mkdirs()
            }

            if (filterJar(jarInput, PluginConfig.argusApmConfig().includes, PluginConfig.argusApmConfig().excludes, PluginConfig.argusApmConfig().excludeJars)) {
                if (inputSourceFileStatus.isAspectChanged) {
                    FileUtils.deleteQuietly(outputJar)

                    val tempAjcWeaver = AjcWeaver()
                    tempAjcWeaver.inPath.add(jarInput.file)
                    tempAjcWeaver.outputJar = outputJar.absolutePath
                    addAjcWeaver(tempAjcWeaver)

                    logCore("jar inputSourceFileStatus.isAspectChanged true")
                } else {
                    if (!outputJar.exists()) {
                        val tempAjcWeaver = AjcWeaver()
                        tempAjcWeaver.inPath.add(jarInput.file)
                        tempAjcWeaver.outputJar = outputJar.absolutePath
                        addAjcWeaver(tempAjcWeaver)
                        logCore("jar inputSourceFileStatus.isAspectChanged false && outputJar.exists() is false")
                    }
                }
            }
        }
    }
}

首先判断aspect文件或者include文件有没有变化,只要有一个发生变化,就需要重新进行“织入”。生成一个新的AjcWeaver对象ajcWeaver,从output provider中获取输出jar文件,然后删除这个jar,把ajcWeaver的outputJar设置成这个jar文件的绝对路径,把include目录的路径,加入到ajcWeaver的inPath列表中。调用addAjcWeaver(ajcWeaver)把ajcWeaver加入到threadPool的taskList中。之后的操作和createTask()方法类似,遍历transformInvocation重的inputs列表,对inputs中的每一个item,遍历它的jarInputs列表,把每一个jarInput的file属性(File类型)添加到classpath中。从transformInvocation的output provider中获取outputJar文件,如果outputJar的父路径不存在,首先创建它的父目录。调用filterJar判断是否有匹配的includes,excludes path或者exclude jar包。如果存在,并且有aspectj文件发生改变,首先删除前边获取的outputJar文件,生成一个AjcWeaver的对象tempAjcWeaver,把jarInputs中当前的条目的file(File类型)属性,添加到tempAjcWeaver的inPath列表中,outputJar的绝对路径absolutePath,然后调用addAjcWeaver(tempAjcWeaver)把tempAjcWeaver添加到threadPool的taskList列表中。如果没有aspectj文件发生改变,并且outputJar不存在,同样先生成一个AjcWeaver的对象tempAjcWeaver,然后把jarInput的file属性,添加到tempAjcWeaver的inPath列表。tempAjcWeaver的outputJar设置成outputJar的绝对路径absolutePath,然后调用addAjcWeaver(tempAjcWeaver)把tempAjcWeaver添加到threadPool的taskList列表中。

回到AspectJTransform的transform中来。前面讨论的是如果有aspectj文件需要织入的情况。如果没有,那么直接调用outputFiles(transformInvocation)将输入源输出到目录下。outputFiles方法的实现很简单,如果transformInvocation支持增量编译,就调用outputChangeFiles(transformInvocation);否则,调用outputAllFiles(transformInvocation)。我们先看一下outputChangeFiles的代码。

fun outputChangeFiles(transformInvocation: TransformInvocation) {
    transformInvocation.inputs.forEach { input ->
        input.directoryInputs.forEach { dirInput ->
            if (dirInput.changedFiles.isNotEmpty()) {
                val excludeJar = transformInvocation.outputProvider.getContentLocation("exclude", dirInput.contentTypes, dirInput.scopes, Format.JAR)
                mergeJar(dirInput.file, excludeJar)
            }
        }

        input.jarInputs.forEach { jarInput ->
            val target = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
            when {
                jarInput.status == Status.REMOVED -> {
                    FileUtils.forceDelete(target)
                }

                jarInput.status == Status.CHANGED -> {
                    FileUtils.forceDelete(target)
                    FileUtils.copyFile(jarInput.file, target)
                }

                jarInput.status == Status.ADDED -> {
                    FileUtils.copyFile(jarInput.file, target)
                }

            }
        }
    }
}

outputChangeFiles方法遍历参数transformInvocation中的inputs列表。对列表中的每一个条目input,首先遍历它的directoryInputs列表,对其中的每一个条目dirInput,判断其中的changedFiles(map)如果不为空,从outputProvider中获取excludeJar,调用mergeJar(dirInput.file, excludeJar)将dirInput.file中的内容,合并到excludeJar中。然后便利input中的jarInputs,对每一个条目jarInput,从outputProvider中获取目标jar包target,判断jarInput.status:如果是Status.REMOVED,直接把target删除;如果是Status.CHANGED,先删除target,再把jarInput.file中的内容,拷贝到target中;如果是Status.ADDED,直接把jarInput.file中的内容,拷贝到target中。

outputAllFiles的代码如下:

fun outputAllFiles(transformInvocation: TransformInvocation) {
    transformInvocation.outputProvider.deleteAll()

    transformInvocation.inputs.forEach { input ->
        input.directoryInputs.forEach { dirInput ->
            val outputJar = transformInvocation.outputProvider.getContentLocation("output", dirInput.contentTypes, dirInput.scopes, Format.JAR)

            mergeJar(dirInput.file, outputJar)
        }

        input.jarInputs.forEach { jarInput ->
            val dest = transformInvocation.outputProvider.getContentLocation(jarInput.name
                    , jarInput.contentTypes
                    , jarInput.scopes
                    , Format.JAR)
            FileUtils.copyFile(jarInput.file, dest)
        }
    }
}

outputAllFiles首先删除outputProvider中的所有内容。遍历参数transformInvocation中的inputs列表,对列表中每一个item input,遍历它的directoryInputs列表,对每一个条目dirInput,从outProvider中根据那么“output”获取outputJar,调用mergeJar,把dirInput.file中的内容,合并到outputJar中。然后遍历input中的jarInputs列表,对每一个条目jarInput,根据jarInput.name,从outputProvider中获取目标文件dest,调用FileUtils.copyFile(jarInput.file, dest),把jarInput.file,拷贝到dest。

到目前为止,我们已经分析完成了transform方法,以及其中涉及的主要类和方法。

下面,我们来看看经常被用到的mergeJar函数:

fun mergeJar(sourceDir: File, targetJar: File) {
    if (!targetJar.parentFile.exists()) {
        FileUtils.forceMkdir(targetJar.parentFile)
    }

    FileUtils.deleteQuietly(targetJar)
    val jarMerger = JarMerger(targetJar)
    try {
        jarMerger.setFilter(object : JarMerger.IZipEntryFilter {
            override fun checkEntry(archivePath: String): Boolean {
                return archivePath.endsWith(SdkConstants.DOT_CLASS)
            }
        })
        jarMerger.addFolder(sourceDir)
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        jarMerger.close()
    }
}

mergeJar首先判断参数targetJar的父目录是否存在,如果不存在,首先创建targetJar的父目录。删除targetJar文件。以targetJar为参数创建一个JarMerger的对象jarMerger。调用jarMerger的setFilter方法,设置filter,filter是一个JarMerger.IZipEntryFilter的具体实现,重写方法checkEntry,判断传入的文件路径是不是以".class"结尾。调用jarMerger.addFolder(sourceDir),最终会触发对文件的读写。
接下来我们逐个分析一下mergeJar中用到的JarMerger中的几个方法。
JarMerger构造函数有一个File类型的参数,jarFile属性代表一个jar文件。JarMerger的init方法还要进行以下初始化操作:

private fun init() {
    if (this.closer == null) {
        FileUtils.forceMkdir(jarFile.parentFile)
        this.closer = Closer.create()
        val fos = this.closer!!.register(FileOutputStream(jarFile))
        jarOutputStream = this.closer!!.register(JarOutputStream(fos))
    }
}

创建jarFile的父目录,创建一个新的Closer对象,并赋值给closer属性。打开jarFile输出流,同时注册到closer,并且返回给jarOutputStream。
addFolder方法首先调用init方法,然后尝试调用addFolderWithPath(folder, “”)。addFolderWithPath方法的代码如下:

@Throws(IOException::class, IZipEntryFilter.ZipAbortException::class)
private fun addFolderWithPath(folder: File, path: String) {
    folder.listFiles()?.forEach { file ->

        if (file.isFile) {
            val entryPath = path + file.name
            if (filter == null || filter!!.checkEntry(entryPath)) {
                // new entry
                this.jarOutputStream!!.putNextEntry(JarEntry(entryPath))

                // put the file content
                val localCloser = Closer.create()
                localCloser.use { localCloser ->
                    val fis = localCloser.register(FileInputStream(file))
                    var count = -1
                    while ({ count = fis.read(buffer);count }() != -1) {
                        jarOutputStream!!.write(buffer, 0, count)
                    }
                }

                // close the entry
                jarOutputStream!!.closeEntry()
            }
        } else if (file.isDirectory) {
            addFolderWithPath(file, path + file.name + "/")
        }
    }
}

addFolderWithPath列出参数folder目录下的所有文件,并且进行遍历。如果当前条目file是一个目录,那么对该目录继续调用addFolderWithPath方法。如果当前条目是一个文件,用参数path再加上file的name属性组成一个新的path服之给entryPath。如果filter为null,或者entryPath应该包含在jar归档文件中,就根据entryPath生成一个JarEntry对象,并且添加到jarOutputStream。创建一个closer的对象localCloser,创建一个file的FileInputStream,并且注册到localCloser,返回输入流给fis。读取file的内容,输出到jarOutputStream绑定的entryPath对应的文件中。输出完成之后,关闭jarOutputStream。

猜你喜欢

转载自blog.csdn.net/foolish0421/article/details/86633030
APM