Javassist basis

Javassist is a class library that manipulates Java bytecodes, adding new methods to an already compiled class, or modifying existing methods, without the need for in-depth knowledge of bytecodes. It can bypass compilation, directly manipulate bytecode, code injection. The best time to use it is after the build tool Gradle has compiled the source files into.class and before it is packaged into.dex.

  • Read and write bytecode: a Javassist.ctclass represents a.class file, so it is key to processing.class.
  • GetClass (className) or classpool. get(class full path). The classPool is the CtClass container that contains a hash table with the class name as the hash key. The classPool object is usually retrieved from classpool.getDefault (), using the JVM’s class search path. If you want to add additional class search paths, you can do so using the pool.insert method, such as pool.insertCLASspath (“user/local/lib”), then add “user/local/lib” to the search path
  • Frozen classes: a class can only be loaded by the JVM once, so if you want to modify a class, unfreeze it, using ctclass.defrost (). This allows you to modify the class and determine whether it isFrozen before unfreezing (ctclass.isfrozen ()).
  • Save and release: If changes are made to a class, it is saved using ctClass.writefile (path). In order to avoid memory overflow (caused by too many CtClass objects), manually delete the object after modification and saving (ctclass.detach ()).
  • Important methods in ctClass: getName(), getAnnotations(), getDeclaredMethod(), getField(), getInterfaces(), getDeclaredConstruct()
  • Insert in the method body code: it through ctClass. GetDeclaredMethod () method returns the method ctMethod, invoke the insertBefore (), insertAfter () and addCatch () method, inserted the code snippet in the method body. Each of the above three methods can receive a String representing a statement or statement block. So the $here has a special meaning. $0 for this, $1, $2, and so on for the first, second, and so on

Trace – free burying point implementation details

It also needs to define a plugin, which, like aspectJ, needs to introduce a framework for operations

apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
    compile gradleApi()
    compile localGroovy()

    compile 'com. Android. Tools. Build: gradle: 3.1.4'

    compile 'org. Javassist: javassist: 3.20.0 - GA'UploadArchives} repositories {jcenter ()} {repositories. MavenDeployer {/ / local repository paths, Repository (URL: uri())'.. /repo') //groupId ='com.sensorsdata'

        //artifactId
        pom.artifactId = 'autotrack.android'// Plug-in version number pom.version ='1.0.0'}}Copy the code

The transform code here is

class SensorsAnalyticsTransform extends Transform {
    private static Project project

    SensorsAnalyticsTransform(Project project) {
        this.project = project
    }

    @Override
    String getName() {
        return "SensorsAnalyticsAutoTrack"} /** * The data types to be processed, there are two enumerated types * CLASSES representing the Java class files to be processed, and RESOURCES representing the RESOURCES to be processed in Java * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        returnTransformmanager.content_class} /** * specifies the Scope of the content to be operated on by a Transform. * 1. EXTERNAL_LIBRARIES only has external libraries * 2. PROJECT only has PROJECT content * 3. PROJECT_LOCAL_DEPS Only has local dependencies of the PROJECT (local JAR) * 4 Provide only local or remote dependencies * 5. SUB_PROJECTS has only subprojects. * 6. SUB_PROJECTS_LOCAL_DEPS has only local dependencies (local JARS) for the subproject. * 7. TESTED_CODE The code tested by the current variable (including dependencies) * @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 JarInputs. Each {TransformInput input -> jar*/ inputs. JarInputs. DestName = jarinput.file. name /** Intercept the md5 value of the file path to rename the output file, because it may be the same name, overwrite */ def hexName = DigestUtils. Md5Hex (jarInput. File. AbsolutePath). The substring (0, 8) / * / * * get the jar nameif (destName.endsWith(".jar")) { destName = destName.substring(0, destName.length() - 4) } File copyJarFile = SensorsAnalyticsInject.injectJar(jarInput.file.absolutePath, project) def dest = outputProvider.getContentLocation(destName + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(copyJarFile, Dest) context. GetTemporaryDir (.) deleteDir directory traversal ()} / * * * / input. DirectoryInputs. Each {DirectoryInput DirectoryInput - > SensorsAnalyticsInject.injectDir(directoryInput.file.absolutePath, project) def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, Directoryinput.scopes, format.directory) /** Copy the input DIRECTORY to the output DIRECTORY */ fileutils.copyDirectory (directoryinput.file, dest) } } } }Copy the code

Here was SensorsAnalyticsInject. InjectJar () and SensorsAnalyticsInject injectDir, namely the dir and deal with the jar. InjectDir is just iterating, executing injectClass

private static void injectClass(File classFile, String path) {
    String filePath = classFile.absolutePath
    if(! filePath.endsWith(".class")) {
        return
    }

    if(! filePath.contains('R$')
            && !filePath.contains('R2$')
            && !filePath.contains('R.class')
            && !filePath.contains('R2.class')
            && !filePath.contains("BuildConfig.class")) {
        int index = filePath.indexOf(path)
        String className = filePath.substring(index + path.length() + 1, filePath.length() - 6).replaceAll("/".".")
        if(! className.startsWith("android")) {try {CtClass CtClass = pool.getctClass (className) // unfreezeif (ctClass.isFrozen()) {
                    ctClass.defrost()
                }

                boolean modified = false

                CtClass[] interfaces = ctClass.getInterfaces()

                if(interfaces ! = null) { Set<String> interfaceList = new HashSet<>()for (CtClass c1 : interfaces) {
                        interfaceList.add(c1.getName())
                    }

                    for (CtMethod currentMethod : ctClass.getDeclaredMethods()) {
                        MethodInfo methodInfo = currentMethod.getMethodInfo()
                        AnnotationsAttribute attribute = (AnnotationsAttribute) methodInfo
                                .getAttribute(AnnotationsAttribute.visibleTag)
                        if(attribute ! = null) {for (Annotation annotation : attribute.annotations) {
                                if ("@com.sensorsdata.analytics.android.sdk.SensorsDataTrackViewOnClick" == annotation.toString()) {
                                    if ('(Landroid/view/View;) V' == currentMethod.getSignature()) {
                                        currentMethod.insertAfter(SDK_HELPER + ".trackViewOnClick(\$1);")
                                        modified = true
                                        break
                                    }
                                }
                            }
                        }

                        String methodSignature = currentMethod.name + currentMethod.getSignature()

                        if ('onContextItemSelected(Landroid/view/MenuItem;) Z' == methodSignature) {
                            currentMethod.insertAfter(SDK_HELPER + ".trackViewOnClick(\$0,\$1);")
                            modified = true
                        } else if ('onOptionsItemSelected(Landroid/view/MenuItem;) Z' == methodSignature) {
                            currentMethod.insertAfter(SDK_HELPER + ".trackViewOnClick(\$0,\$1);")
                            modified = true
                        } else {
                            SensorsAnalyticsMethodCell methodCell = SensorsAnalyticsConfig.isMatched(interfaceList, methodSignature)
                            if(methodCell ! = null) { StringBuffer stringBuffer = new StringBuffer() stringBuffer.append(SDK_HELPER) stringBuffer.append(".trackViewOnClick(")
                                for (int i = methodCell.getParamStart(); i < methodCell.getParamStart() + methodCell.getParamCount(); i++) {
                                    stringBuffer.append("\ $")
                                    stringBuffer.append(i)
                                    if(i ! = (methodCell.getParamStart() + methodCell.getParamCount() - 1)) { stringBuffer.append(",")
                                    }
                                }
                                stringBuffer.append(");")
                                currentMethod.insertAfter(stringBuffer.toString())
                                modified = true}}}}if(Modified) {ctClass.writefile (path) ctClass.detach()// Release}} Catch (Exception e) {e.printStackTrace()}}}}Copy the code

(1) Exclude classes that do not need to be handled, such as r.class; (2) retrieve all interfaces implemented by the class; (3) Iterate over all methods in the class if any interface is implemented; (4) If the method’s signature matches the current one (e.g. OnContextItemSelected (Landroid/view/MenuItem;) Z) or add an annotation, insertAfter (XXX) code is executed, of course XXX is defined 5: save and free if you modify the file

The rest is to define the plugin and generate the Resource file, which is omitted here.

Attribute value passing, annotation value fetching

The first step is to define the plugin and transform, which are the same code as above. The first step is to insert the code into a specific method, such as a public void test() method. Iterate through all of its methods, and then use the method name and signature to determine if it is the same method

String methodSignature = currentMethod.name + currentMethod.getSignature()
System.out.println("methodSignature = " + methodSignature)
if ('test()V' == methodSignature) {
    currentMethod.insertAfter(SDK_HELPER + ".injectLog();")
    modified = true
}
Copy the code

Where, the code in MainActivity is

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        test(a); } public voidtest() {
        Log.e("tag"."This is a test."); }}Copy the code

The helper code is

public class InjectMethodHelper {

    public static void injectLog() {
        Log.e("tag"."This is a little bit of dotting code that was inserted.");
    }

    public static void injectAnnotationLog(String id, String value) {
        Log.e("Tag"."This is an inserted comment dot code ===>>id====>>" + id + "=====>>value====>>"+ value); }}Copy the code

You can see the log at build time

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
    String id();

    String value();

}
Copy the code

Declare a method in mainActivity and call it in onCreate

@AnnotationTest(id = "idNo.1", value = "valueNo.1")
public void testAnnotation() {
    Log.e("tag"."This is an annotated method.");
}

Copy the code

When you’re dealing with bytecode, you need to deal with annotations as follows

for(CtMethod currentMethod: ctClass getDeclaredMethods ()) {/ / get all the annotation Object [] annotations. = currentMethod getAnnotations ()if(annotations ! = null && annotations.length > 0) { MethodInfo methodInfo = currentMethod.getMethodInfo() AnnotationsAttribute attribute = (AnnotationsAttribute) methodInfo. GetAttribute (AnnotationsAttribute visibleTag) / / to get the needed to annotate the Annotation Annotation = attribute.getAnnotation(ANNOTATION_HELPER)if(annotation ! = null) {/ / annotations defined in the name of the value of the def names. = the annotation getMemberNames () / / get the value of the annotation String id = annotation. GetMemberValue ("id")
            String values = annotation.getMemberValue("value"/ / printlogThis will print system.out.println ("annnotationMemberNames =" + names.toString())
            System.out.println("annnotationMemberId =" + id)
            System.out.println("annnotationMemberValue ="+ values) // used when passing the resulting value to a method${value}In this form, StringBuffer StringBuffer = new StringBuffer() stringBuffer.append(SDK_HELPER) stringBuffer.append(".injectAnnotationLog(")
            stringBuffer.append("${id}")
            stringBuffer.append(",")
            stringBuffer.append("${values}")
            stringBuffer.append(");")
            System.out.println(stringBuffer.toString())
            currentMethod.insertAfter(stringBuffer.toString())

            modified = true

        }

    }

    String methodSignature = currentMethod.name + currentMethod.getSignature()
    System.out.println("methodSignature = " + methodSignature)
    if ('test()V' == methodSignature) {
        currentMethod.insertAfter(SDK_HELPER + ".injectLog();")
        modified = true}}Copy the code

The above comments already very understand, that is, through the MethodInfo get all runtime annotations Attribute, and then use this Attribute to specify the Annotation, through getMemberValue can access to the value of the specified Annotation directly write died here, if you don’t write to death, Get the immediate name by getMemberNames, and then use the name here to fetch the value of the annotation. Note that the insertAfter method is passing in a String created using StringBuilder, because we need to pass in the value of the property we just got to the fixed method, and the compiler will give an error if we use String+String. At this point we can look at the build process to print the log

private static void injectClass(File classFile, String path) {
    String filePath = classFile.absolutePath
    if(! filePath.endsWith(".class")) {
        return
    }

    if(! filePath.contains('R$')
            && !filePath.contains('R2$')
            && !filePath.contains('R.class')
            && !filePath.contains('R2.class')
            && !filePath.contains("BuildConfig.class")) {
        int index = filePath.indexOf(path)
        String className = filePath.substring(index + path.length() + 1, filePath.length() - 6).replaceAll("/".".")
        if(! className.startsWith("android")) {try {CtClass CtClass = pool.getctClass (className) // unfreezeif (ctClass.isFrozen()) {
                    ctClass.defrost()
                }

                boolean modified = false

                for(CtMethod currentMethod: ctClass getDeclaredMethods ()) {/ / get all the annotation Object [] annotations. = currentMethod getAnnotations ()if(annotations ! = null && annotations.length > 0) { MethodInfo methodInfo = currentMethod.getMethodInfo() AnnotationsAttribute attribute = (AnnotationsAttribute) methodInfo. GetAttribute (AnnotationsAttribute visibleTag) / / to get the needed to annotate the Annotation Annotation = attribute.getAnnotation(ANNOTATION_HELPER)if(annotation ! = null) {/ / annotations defined in the name of the value of the def names. = the annotation getMemberNames () / / get the value of the annotation String id = annotation. GetMemberValue ("id")
                            String values = annotation.getMemberValue("value"/ / printlogThis will print system.out.println ("annnotationMemberNames =" + names.toString())
                            System.out.println("annnotationMemberId =" + id)
                            System.out.println("annnotationMemberValue ="+ values) // used when passing the resulting value to a method${value}In this form, StringBuffer StringBuffer = new StringBuffer() stringBuffer.append(SDK_HELPER) stringBuffer.append(".injectAnnotationLog(")
                            stringBuffer.append("${id}")
                            stringBuffer.append(",")
                            stringBuffer.append("${values}")
                            stringBuffer.append(");")
                            System.out.println(stringBuffer.toString())
                            currentMethod.insertAfter(stringBuffer.toString())

                            modified = true

                        }

                    }

                    String methodSignature = currentMethod.name + currentMethod.getSignature()
                    System.out.println("methodSignature = " + methodSignature)
                    if ('test()V' == methodSignature) {
                        currentMethod.insertAfter(SDK_HELPER + ".injectLog();")
                        modified = true}}if(Modified) {ctClass.writefile (path) ctClass.detach()// Release}} Catch (Exception e) {e.printStackTrace()}}}}Copy the code