argus-apm-gradle工程定义了一个gradle plugin,主要有以下两个作用:
- 支持AOP编程,方便ArgusAPM能够在编译期织入一些性能采集的代码;
- 通过Gradle插件来管理依赖库,使用户接入ArgusAPM更简单。
argus-apm-gradle使用kotlin语言开发。这里我们假定大家已经熟悉gradle plugin的开发。
AspectJPlugin
esources/META-INF/gradle-plugins/目录下的argusapm.properties指明了plugin的入口:argus-apm-gradle使用kotlin语言开发。这里我们假定大家已经熟悉gradle plugin的开发。 resources/META-INF/gradle-plugins/目录下的argusapm.properties指明了plugin的入口:
implementation-class=com.argusapm.gradle.AspectJPlugin
下面来看下AspectJPlugin这个类的代码:
internal class AspectJPlugin : Plugin {
private lateinit var mProject: Project
override fun apply(project: Project) {
mProject = project
// 对应在build中argusApmAjxConfig这个extention.
project.extensions.create(AppConstant.USER_CONFIG, ArgusApmConfig::class.java)
//公共配置初始化,方便获取公共信息
PluginConfig.init(project)
//自定义依赖库管理
project.gradle.addListener(ArgusDependencyResolutionListener(project))
project.repositories.mavenCentral()
// kotlin的扩展
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 这个类实现了Plugin接口,并实现了apply方法。在apply方法中首先project这个参数将mProject 属性初始化。随后通过project.extensions.create()方法接受了ArgusApmConfig 中定义的参数,create()的第一次参数是使用插件项目的build.gradle中进行参数配置的DSL的名字。然后调用PluginConfig.init() 方法进行初始化。而project.gradle.addListener(ArgusDependencyResolutionListener(project))这句实现了自定义的依赖库管理,addListener()方法接受的参数类型如下:
*
* - {@link org.gradle.BuildListener}
*
- {@link org.gradle.api.execution.TaskExecutionGraphListener}
*
- {@link org.gradle.api.ProjectEvaluationListener}
*
- {@link org.gradle.api.execution.TaskExecutionListener}
*
- {@link org.gradle.api.execution.TaskActionListener}
*
- {@link org.gradle.api.logging.StandardOutputListener}
*
- {@link org.gradle.api.tasks.testing.TestListener}
*
- {@link org.gradle.api.tasks.testing.TestOutputListener}
*
- {@link org.gradle.api.artifacts.DependencyResolutionListener}
*
这些listener对应gradle编译过程中的各个节。而上面提到的ArgusDependencyResolutionListener实现了DependencyResolutionListener接口,这个是依赖相关的listener。所以ArgusDependencyResolutionListener的beforeResolve会在项目的依赖确定之前调用执行。project.repositories.mavenCentral()将仓库指向了默认的maven central。 project.compatCompile() 这个是通过kotlin的扩展功能为Project临时扩展的函数,最终添加了aspectjrt依赖。如果使用plugin有AppPlugin(即apply plugin: 'com.android.application' ,就是applictaion所在的的工程,而非library)。然后注册了BuildTimeListener用来监听项目编译的时间等。最后获取Extension 的具体类型,并赋值给android,最后android注册了AspectJTransform。
AspectJTransform
接下来讲AspectJTransform,对于Transform类型的task,主要关注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()方法中首先定义了一个TransformTask类型的变量transformTask,然后用transformInvocation的属性对其进行初始化,之后开始输出log。FileFilter是用于指明当前transform这个task 所用到的路径。然后定义并初始化了 InputSourceCutter,之后调用startCut()。下面看下InputSourceCutter这个类:
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是否为isIncremental。如果是,就遍历transformInvocation的input列表。然后遍历每个input的directoryInputs和jarInputs,分别调用whenDirInputsChanged()和whenJarInputsChanged()。如果不是isIncremental,则首先将transformInvocation的outputProvider的所有内容删除,之后再遍历transformInvocation.inputs。然后遍历每个input中的directoryInputs和jarInputs,分别调用cutDirInputs()和cutJarInputs()。
InputSourceCutter中定义了一个ThreadPool类型的属性taskManager,ThreadPool类维护了一个定长线程池。
接下来我们来看下init中的四个函数的具体作用,首先我们看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, 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?, scopes, Format.JAR)
FileUtils.deleteQuietly(excludeOutputJar)
mergeJar(getExcludeFileDir(), excludeOutputJar)
}
return null
}
})
}
whenDirInputsChanged会往TaskManager中添加一个新的task,这个task执行的内容是:遍历dirInput中发生变化的目录,然后每个变动的文件执行fileFilter的whenAJClassChangedOfDir()和whenClassChangedOfDir()方法。然后如果属性inputSourceFileStatus.isIncludeFileChanged的值为true,即include files发生变化,则删除include输出jar。如果inputSourceFileStatus的isExcludeFileChanged属性为true,即exclude files发生变化,则重新生成exclude jar到输出目录。
然后看下第二个函数,whenJarInputsChanged的代码如下:
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的文件发生了变化。这个时候向taskManager中添加task,这个task依次调用fileFilter的whenAJClassChangedOfJar()和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, scopes, Format.JAR)
mergeJar(getExcludeFileDir(), excludeJar)
}
return null
}
})
}
这个函数也想taskManager中添加一个task,这个task 的作用是遍历dirInput的每一个文件,然后对每个文件调用fileFilter的filterAJClassFromDir()和filterClassFromDir()方法。如果getExcludeFileDir中有文件,则调用mergeJar将getExcludeFileDir中的文件merge之后放到excludeJar。
最后看下第四个方法。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()。
分析完InputSourceCutter 的init方法后,回到AspectJTransform的transform方法 。在InputSourceCutter 初始化后调用了startCut(),这个方法调用了TaskManager的startWork(),开启了TaskManager中的所有task.
FileFilter
前面调用了FileFilter的很多方法,现在分析下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都在这个路径下。 接下来我们看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()这个方法来判断是不是aspect class文件 。isAspectClassFile()首先判断文件是不是class文件。然后通过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
}
上面这个方法用到了ASM框架,ASM是一个JAVA字节码操控框架,能用到动态生成类或者增强已有类的功能。classReader和classWriter都来源于该框架。
classReader类可以直接由字节数组或者class文件间接获得字节码数据,能正常分析字节码,构建出抽象的树在内存中表示字节码。它会调用accept()方法,这个方法接受一个实现了ClassVisitor接口的对象实例作为参数。然后依次调用ClassVisitor接口的各个方法。字节码空间上的偏移被转换成visit 事件在时间上调用的先后,所谓visit事件是指各种不同visit函数的调用,ClassReader知道如何调用各种visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的visitor来对字节码树进行不同的修改。ClassVisitor会产生一些子过程,比如visitMethod会返回一个实现了MethodVisitor接口的实例,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,那么FileFilter的isAspectClassFile()方法会返回true.whenAJClassChangedOfDir()方法接下来会给inputSourceFileStatus的属性isAspectChanged置为true.标记Aspect class发生了变化,并且设置path和subPath.接下来会根据变化的status,采取下一步的操作。如果是Status.REMOVED,就删除cacheFile.如果是Status.CHANGED,就先删除cacheFild,然后调用cache()方法生成新的cache文件。如果状态是Status.ADDED,则直接调用cache()生成新的cache文件。
接下来看FileFilter的第二个函数,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}")
}
}
}
调用 isIncludeFilterMatched和isExcludeFilterMatched,判断include路径和exclude路径是否匹配。如果匹配include路径但不匹配exclude路径,而且 inputSourceFileStatus.isIncludeFileChanged原来的值是false,就把inputSourceFileStatus.isIncludeFileChanged标记为true.如果匹配exclude路径,并且inputSourceFileStatus.isExcludeFileChanged的值为false,就将inputSourceFileStatus.isExcludeFileChanged的值标记为true.接下来生成一个target文件,根据之前的isInclude的值决定生成incude还是exclude文件。最后根据参数status的值,对target文件执行不同的操作。
接下来我们看第三个函数,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.输出到变量byte中。然后生成一个cache文件。文件路径是aspect目录的路径+当前这个条目对应的class文件的名字。如果是一个aspect文件,就将inputSourceFileStatus.isAspectChanged标记为true.然后根据jarInput的status来对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.决定对outputJar的动作。最后对不需要做AOP处理的文件原样copy到输出目录。
接下来,我们继续分析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路径是否匹配include路径或者exclude路径。如果匹配includes路径,就调用cache方法,将file中的内容,输出到一个新的文件,文件路径是includeDirPath + subPath。如果匹配exclude路径,就调用cache方法,将file中的内容输出到新的文件,不过文件路径是excludeDirPath + 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中,最后关闭jar文件。
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代码织入,直接拷贝到目标文件目录即可。
以上四个函数名字中都包含了“filter”,在执行目标操作之前都要进行下filter操作。
到目前为止,我们实际上分析完了AspectJTransform类中transform方法的第一步。接下来分析第二步。
transform方法第二步
首先判断PluginConfig.argusApmConfig().enabled 是否为true,然后判断第一步生成的文件是否包含aspect文件。如果两个条件都满足,就用transformInvocation和inputSourceFileStatus 生成一个AjcWeaverManager的对象。并且调用该对象的weave方法,否则就调用outputFiles()方法输出文件内容,这个方法最后再分析。
AjcWeaverManager的默认构造函数有两个参数 TransformInvocation类型和 InputSourceFileStatus类型。这两个参数用来初始化类的两个属性。另外类中还有以下属性:
private val threadPool = ThreadPool()
private val aspectPath = arrayListOf()
private val classPath = arrayListOf()
threadPool定义了一个线程池,aspectPath和classPath分别用来存储aspect文件和class文件路径的列表。weave()的代码如下:
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,允许多线程运行aspectj.如果支持增量编译,就调用createIncrementalTask()方法,创建增量编译任务。否则调用createTask()方法,创建普通的全量编译任务。接下来遍历threadPool的taskList. taskList的内容由createIncrementalTask()或createTask()添加。对TaskList中的每一项都设置好参数之后,最后通过threadPool.startWork()启动线程。
接下来,先看下createTask()方法,代码如下:
private fun createTask() {
val ajcWeaver = AjcWeaver()
val includeJar = transformInvocation.outputProvider.getContentLocation("include", contentTypes as Set, 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,获取includeJar的路径。将includeJar路径下的文件删除后,将ajcWeaver的outputJar 设置为includeJar的绝对路径。将getIncludeFileDir()添加到ajcWeaver的inPath列表中。随后调用addAjcWeaver将AjcWeaver这个task添加到threadPool的task列表中。最后遍历transformInvocation,对其中的每一项遍历其jarInputs列表。将每一个jar文件添加到classPath()中,然后调用filterJar()方法判断该jar文件是否需要参与AJC织入,如果需要参与,创建新的AjcWeaver对象tempAjcWeaver,设置参数后将tempAjcWeaver添加到threadPool的task列表中。
然后再看下createIncrementalTask()方法,其代码如下:
private fun createIncrementalTask() {
//如果AJ或者Include文件有一个变化的话,则重新织入
if (inputSourceFileStatus.isAspectChanged || inputSourceFileStatus.isIncludeFileChanged) {
val ajcWeaver = AjcWeaver()
val outputJar = transformInvocation.outputProvider?.getContentLocation("include", contentTypes as Set, 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")
}
}
}
}
}
}
首先判断AJ或者Include文件是否有变化,如果有一个有变化,就需要重新织入。生成一个新的AjcWeaver对象ajcWeaver,设置参数后将ajcWeaver添加到线程池的task列表中。之后遍历transformInvocation的input列表,对每一项遍历jarInputs 列表,将每一个jarInput的file属性添加到classpath中。随后调用filterJar()判断是否有匹配的includes,exclude path或exclude jar包。如果存在,且有aspect文件发生改变,则新建AjcWeaver对象tempAjcWeaver,设置参数后将其加入到线程池的task列表中。如果没有aspect文件发生改变,并且outputJar文件不存在。同样新建AjcWeaver对象tempAjcWeaver,设置参数后将其添加到线程池的task列表。
AjcWeaver集成自ITask。其call()代码如下:
override fun call(): Any? {
val log = PluginConfig.project.logger
val args = mutableListOf(
"-showWeaveInfo",
"-encoding", encoding,
"-source", sourceCompatibility,
"-target", targetCompatibility,
"-classpath", classPath.joinToString(File.pathSeparator),
"-bootclasspath", bootClassPath
)
if (!inPath.isEmpty()) {
args.add("-inpath")
args.add(inPath.joinToString(File.pathSeparator))
}
if (!aspectPath.isEmpty()) {
args.add("-aspectpath")
args.add(aspectPath.joinToString(File.pathSeparator))
}
if (outputDir != null && !this.outputDir!!.isEmpty()) {
args.add("-d")
args.add(outputDir)
}
if (outputJar != null && !outputJar!!.isEmpty()) {
args.add("-outjar")
args.add(outputJar)
}
if (!ajcArgs.isEmpty()) {
if (!ajcArgs.contains("-Xlint")) {
args.add("-Xlint:ignore")
}
if (!ajcArgs.contains("-warn")) {
args.add("-warn:none")
}
args.addAll(ajcArgs)
} else {
args.add("-Xlint:ignore")
args.add("-warn:none")
}
val handler = MessageHandler(true)
val m = Main()
args.forEach {
com.argusapm.gradle.internal.utils.log("$it")
}
LogStatus.weaveStart()
m.run(args.toTypedArray(), handler)
handler.getMessages(null, true).forEach { message ->
when (message.kind) {
IMessage.ABORT, IMessage.ERROR, IMessage.FAIL -> {
log.error(message.message, message.thrown)
throw GradleException(message.message, message.thrown)
}
IMessage.WARNING -> {
log.warn(message.message, message.thrown)
}
IMessage.INFO -> {
log.info(message.message, message.thrown)
}
IMessage.DEBUG -> {
log.debug(message.message, message.thrown)
}
}
}
return null
}
前面主要是对参数的完善,关键的一句代码是:
m.run(args.toTypedArray(), handler)
这个方法最终会调用Main这个类的run()方法。在这个方法中最终会调用ajc库中的AjdtCommand类中的runCommand().
回到AspectJTransform的transform()方法中来,前面讨论的是如果需要织入的情况。如果不需要织入,则调用outputFiles()方法将输入源输出到目录下,接下来分析outputFiles这个方法。
fun outputFiles(transformInvocation: TransformInvocation) {
if (transformInvocation.isIncremental) {
outputChangeFiles(transformInvocation)
} else {
outputAllFiles(transformInvocation)
}
}
如果支持增量编译,就调用outputChangeFiles(),否则调用outputAllFiles().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)
}
}
}
}
}
首先遍历transformInvocation的inputs列表,对列表中的每一项,遍历它的directoryInputs列表。对其中每一个dirInput,判断dirInput其中的changedFiles是否为空。如果不为空,就从outputProvider 中获取excludeJar。随后调用mergeJar将dirInput.file中的内容合并到excludeJar中。然后遍历input的每一个jarInput,从outputProvider中获取目标jar包赋值给target,然后根据jarinput的状态分别对target进行操作。
outputFiles中如果不支持增量,就会调用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)
}
}
}
首先将transformInvocation的outputProvider()的所有内容删除。遍历transformInvocation的inputs,对其中的每一个item input.遍历directoryInputs列表,从outputProvider中取出output的内容,赋值给outputJar,调用mergeJar将dirInput.file中的内容合并到outputJar。然后对遍历每个input的jarInputs列表。对每一项jarInput,根据name,contentTypes,scopes从outputProvider中获取目标文件dest.最终调用FileUtils.copyFile()将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文件删除,以target为参数构建一个JarMerger对象,然后调用jarMerger的setFilter方法。filter是一个JarMerger.IZipEntryFilter的具体实现,重写checkEntry方法。判断传入的文件路径是否以“.class”结尾。最后调用jarMerge的addFolder,最终会触发对文件的读写。
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的父目录,创建一个新的Close对象,并赋值给close属性,打开JarFile输出流,同时注册到closer,之后返回给jarOutputStream。
mergerJar中的addFolder首先调用init方法,然后尝试调用addFolderWithPath,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 + "/")
}
}
}
首先对folder下的文件进行遍历,如果当前条目是一个目录,那么对该目录继续调用addFolderWithPath()方法,否则就用参数path加上file的name属性主城一个新的path,然后赋值给entryPath。如果filter为空,或者entryPath应该包含在jar归档文件中,就根据entryPath生成一个JarEntry的对象,并添加到jarOutputStream中,之后创建一个closer的对象localCloser,创建一个file的FileInputStream并注册到localCloser,返回输入流给fis。读取file文件的内容,输出到jarOutputStream绑定的enterPath对应的文件中,输出完成后关闭jarOutputStream。
有什么不对的地方,请大家不吝指教。
学习的过程中,参考了网上的资料,感谢先行者的经验总结和无私分享。
参考文献:
360 Argus APM 源码分析(2)—— argus-apm-gradle源码分析