argus-apm-gradle源码分析
argus-apm-gradle工程定义了一个gradle plugin,主要有以下两个作用:
- 支持AOP编程,方便ArgusAPM能够在编译期织入一些性能采集的代码;
- 通过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()
。
下面是一个目录的实际的例子:
##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。