Java字节码以二进制的形式存储在.class文件中,每一个class文件包含一个Java类或接口。Javassist框架就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者修改已有的方法,并且不需要对字节码方面有深入的了解。
Javassist可以绕过编译,直接操作字节码,从而实现代码的注入。所以,使用Javassist框架的最佳时机就是在构建工具gradle将源文件编译成class文件之后,在将class打包成dex文件之前。
ClassPool pool=ClassPool.getDefault();
CtClass aClass = pool.get("com.test.A");
aClass.setSuperclass("java.lang.Object");
aClass.writeFile();
在上面这个例子中,我们首先获取一个ClassPool对像。ClassPool是CtClass对像的容器,可以按需读取类文件用来创建并保存CtClass对像,以便之后可能会被使用到。
为了修改类的定义,首先需要使用ClassPool.get()方法从ClassPool中获取一个CtClass对像。使用getDefault方法获取的ClassPool对像使用的是默认系统的类搜索路径。
ClassPool是一个存储CtClass的Hash表,类的名称作为Hash表的key。ClassPool的get方法会从Hash表查找key对应的CtClass对像。如果根据对应的key没有找到CtClass对像,get方法就会创建并返回一个新的CtClass对像,这个对像同时也会保存在Hash表中。
从ClassPool中获取的CtClass对像是可以被修改的。在上面的例子中,将A类的父类改为Object。调用writeFile方法后,这项修改会被写入原始类文件中。writeFile方法会将CtClass对象转换成类文件并写到本地磁盘。同时,也可以使用toBytecode方法来获取修改过的字节码。比如:
byte[] b = aClass.toBytecode();
也可以使用toClass方法直接将CtClass对象转换成Class对象,比如:
Class clazz= aClass.toClass();
toClass方法请求当前线程的ClassLoader加载CtClass对象所代表的类文件,它返回的是该类文件的Class对象。
aClass.writeFile();
aClass.defrost();//解冻
aClass.setSuperClass();//因为这个类已经解冻了,所以可以更改该类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassPath(this.getClass()));
在上面的代码示例中,将this指向的类添加到ClassPool的类加载路径中。你可以使用任意Class对象来代替this.getClass(),从而将Class对象添加到类加载路径中,也可以注册一个目录作为搜索路径。比如:
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/Library");
ClassPool
ClassPool是CtClass对象的容器。因为编译器在编译引用CtClass代表的Java类的源代码时,可能会引用CtClass对象,所以一旦一个CtClass被创建,它就会被保存在ClassPool中。
避免内存溢出
如果CtClass对象的数量变得非常多,ClassPool有可能会导致巨大的内存消耗。为了避免这个问题,我们可以从ClassPool中显式删除不必要的CtClass对象。如果对CtClass对象调用detach方法,那么该CtClass对象将会从ClassPool中删除。比如:
CtClass aClass = ...;
aClass.writeFile();
aClass.detach();
在调用detach方法之后,就不能再调用这个CtClass对象的任何有关方法了。如果调用了ClassPool的get方法,ClassPool会再次读取这个类文件,并创建一个新的CtClass对象。
注解(Annotations)
CtClass、CtMethod、CtField和CtConstructor均提供了getAnnotations方法,用于读取对应类型上添加的注解。
在方法体中插入代码
CtMethod和CtConstructor均提供了insertBefore、insertAfter、addCatch等方法。它们可以把用Java编写的代码片段插入到现有的方法体中。Javassist包括一个用于处理源代码的小型编译器,它接收用Java编写的源代码,然后将其编译成Java字节码,并内联到方法体中。
也可以按行号来插入代码段(如果行号表包含在类文件中)。向CtMethod和CtConstructor中的insertAt方法提供源代码和原始类定义中的源文件的行号,就可以将编译后的代码插入到指定行号位置。
insertBefore、insertAfter、insertAt、addCatch等方法都能接收一个表示语句或语句块的String对象。一个语句是一个单一的控制结构,比如if和while,或者以分号结尾的表达式。语句块是一组用{}包围的语句。
语句和语句块可以引用字段和方法,但不允许访问在方法中声明的局部变量,尽管在块中声明一个新的局部变量是允许的。
传递给方法insertBefore、insertAfter、addCatch、insertAt的String对象是由Javassist的编译器编译的。
由于编译器支持语言扩展,所以以$开头的几个标识符都有特殊的含义:
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{System.out.println($e);throw $e}",etype);
转换成对应的java代码如下:
try{
//the original method body
}catch(java.io.IOException e){
System.out.println(e);
throw e;
}
注意,插入的代码片段必须以throw或return语句结束。
首先,创建一个buildSrc文件夹,引入依赖
apply plugin: 'groovy'//gradle会根据插件的名称,找到这个插件并且调用插件里面的apply方法
repositories {
maven { url 'https://maven.aliyun.com/repository/google' }
maven {
url 'https://maven.aliyun.com/nexus/content/groups/public/'
}
// jcenter()
}
sourceSets {
main {
groovy {
srcDir 'src/main/groovy'
}
}
}
dependencies {
// implementation gradleApi() //buildSrc目录下可以不引用gradleApi
implementation 'com.android.tools.build:gradle:3.4.3'
}
group 'com.brett.gradle'
//version '1.0.2'
然后,在resources文件夹下面创建一个properties
implementation-class=com.brett.gradle.ReleaseHelperPlugin
创建一个插件:
package com.brett.gradle;
import com.android.build.gradle.AppExtension;
import com.android.build.gradle.api.ApplicationVariant
import com.brett.gradle.tasks.GenerateApkTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.invocation.Gradle
import java.util.regex.Matcher
import java.util.regex.Pattern;
class ReleaseHelperPlugin implements Plugin<Project> {
private static final String sPluginExtensionName = "releaseHelper";
private static final String ANDROID_EXTENSION_NAME = "android";
private Project project;
@Override
public void apply(Project project) {
this.project = project;
// project.getExtensions().create(sPluginExtensionName, CustomExtension.class,project);
project.getExtensions().findByType(AppExtension).registerTransform(new ReleaseHelperTransform(project))
}
}
class ReleaseHelperTransform extends Transform {
private static Project project
ReleaseHelperTransform(Project project) {
this.project = project
}
@Override
String getName() {
return "ReleaseHelperTransform"
}
/**
* 需要处理的数据类型,有两种枚举类型
* CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
* 1. EXTERNAL_LIBRARIES 只有外部库
* 2. PROJECT 只有项目内容
* 3. PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
* 4. PROVIDED_ONLY 只提供本地或远程依赖项
* 5. SUB_PROJECTS 只有子项目。
* 6. SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
* 7. TESTED_CODE 由当前变量(包括依赖项)测试的代码
* @return
*/
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
if (!incremental) {
outputProvider.deleteAll()
}
/**Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历 */
inputs.each { TransformInput input ->
/**遍历 jar*/
input.jarInputs.each { JarInput jarInput ->
/**重命名输出文件(同目录copyFile会冲突)*/
String destName = jarInput.file.name
/**截取文件路径的 md5 值重命名输出文件,因为可能同名,会覆盖*/
def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
/** 获取 jar 名字*/
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length() - 4)
}
def dest = outputProvider.getContentLocation(destName + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
context.getTemporaryDir().deleteDir()
}
/**遍历目录*/
input.directoryInputs.each { DirectoryInput directoryInput ->
CodeInjects.inject(directoryInput.file.absolutePath, project)
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
/**将input的目录复制到output指定目录*/
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
}
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project
public class CodeInjects {
private final static ClassPool pool = ClassPool.getDefault();
public static void inject(String path, Project project){
//当前路径加入类池,不然找不到这个类
pool.appendClassPath(path)
//project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
pool.appendClassPath(project.android.bootClasspath[0].toString())
File dir = new File(path)
if(dir.isDirectory()){
//遍历目录
dir.eachFileRecurse {File file->
String filePath = file.absolutePath
println("CodeInjects filePath:"+filePath)
if(file.getName().equals("MainActivity.class")){
//获取MainActivity.class
CtClass ctClass = pool.getCtClass("com.sogou.teemo.test_use_gradle_plugin.MainActivity");
println("CodeInjects ctClass = "+ctClass)
if(ctClass.isFrozen()){
ctClass.defrost()
}
//获取到onCreate方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate");
println("CodeInjects 方法名 = " + ctMethod)
String insetBeforeStr = """ android.widget.Toast.makeText(this,"插件中自动生成的代码",android.widget.Toast.LENGTH_SHORT).show();
"""
ctMethod.insertAfter(insetBeforeStr)
ctClass.writeFile(path)
ctClass.detach()//释放
}
}
}
}
}
https://github.com/jboss-javassist/javassist/wiki/Tutorial-1