Automatically scan implementation classes of java interfaces and generate registry

Keywords: Gradle Android Maven Java

Preface

Recently, the framework of android component library was set up in our company. The general idea is to provide a base library. Each component (the implementation class of IComponent interface) is registered in the component management class (ComponentManager class). When components are in the same app, they can communicate through Component Manager by calling request forwarding (the way of communication between different apps is not the subject of this article.Leave it out for now.However, a problem was encountered during the implementation:

How do I register component classes from different module s into management classes?

The first thing that came to mind was the need to provide a registration method in ComponentManager by:

  1. The registration method is register(IComponent component), called in the application.onCreate method of the app module, import the corresponding IComponent implementation class, and call its constructor to perform the registration.
  2. The registration method is register(String componentClassName), which invokes its construction method with reflection in the register method, and invokes and passes in the full class name of the IComponent implementation class in the application.onCreate method of the app module
    • Full class names can be written directly in java code or a list of component class names can be read from a configuration file
  3. The registration method is register(String componentClassName), which is called the first time ComponentManager calls the corresponding component, in which the component's call must specify the full class name of the component being called

The disadvantage of Mode 1 is that the code is too intrusive to import each component class in the application
Mode 2 and 3 Component implementation classes require configuration code to confuse keep
There is a common problem with all the above methods: the component registration list must be maintained manually and modified accordingly for each component addition/deletion/change.

Is there a way to automatically manage this list?

Associate it with a previous way of automatically generating code with asm AndAop The plug-in is used to automatically insert the specified code into any method of any class, so a gradle plug-in is written that automatically generates the registered component.
The general idea is that at compile time, all classes are scanned, qualified classes are collected, and registration code is generated into the static block of the specified class, which enables automatic registration at compile time without any concern for which component classes are in the project.

Performance: Due to the use of more efficient ASM frameworks for byte code analysis and modification, and the filtering of all classes in the android/support package (also supports setting custom scan ranges), all dex files totaled about 12MB before no code confusion, and the total time consumed for scanning and code insertion was between 2S and 3s.

After the plug-in development was completed, a colleague suggested that he also had a module implemented by the policy mode that required similar registration function. Considering the versatility of this plug-in function, the upgrade component auto-registration plug-in was made a generic auto-registration plug-in. AutoRegister , supports configuring multiple types of scan registrations, as shown in github README Document.

Implementation process

Step 1: Preparation

  1. First thing to know How to develop Gradle plug-ins using Android Studio
  2. Understand that the Transform API: Transform API is provided after Gradle 1.5.0 and allows third parties to modify java byte codes during compilation before packaging Dex files (custom plug-in registered transforms execute before ProguardTransform and DexTransform, so auto-registered classes do not need to be confused). Reference articles include:
  3. Byte Code Modification Framework (ASM is harder to get started with than Javassist Framework, but has higher performance, but learning difficulty doesn't stop us from pursuing performance):

Step 2: Build a plug-in project

  1. according to How to develop Gradle plug-ins using Android Studio The method in the article creates the plug-in project and publishes it to the local maven repository (I placed it in a folder under the project root directory) so that we can quickly debug it locally

Part of the build.gradle file is as follows:

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

repositories {
    mavenCentral()
}
dependencies {
    compile 'com.android.tools.build:gradle:2.2.0'
}


//Load the local maven private service configuration (configure in the local.properties file in the project root)
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def artifactory_user = properties.getProperty("artifactory_user")
def artifactory_password = properties.getProperty("artifactory_password")
def artifactory_contextUrl = properties.getProperty("artifactory_contextUrl")
def artifactory_snapshot_repoKey = properties.getProperty("artifactory_snapshot_repoKey")
def artifactory_release_repoKey = properties.getProperty("artifactory_release_repoKey")

def maven_type_snapshot = true
// The version number referenced by the project, such as 1.0.1 in compile'com.yanzhenjie:andserver:1.0.1', is configured here.
def artifact_version='1.0.1'
// Unique package names, such as com.yanzhenjie in compile'com.yanzhenjie:andserver:1.0.1'are configured here.
def artifact_group = 'com.billy.android'
def artifact_id = 'autoregister'
def debug_flag = true //true: publish to local maven repository, false: publish to Maven private service

task sourcesJar(type: Jar) {
    from project.file('src/main/groovy')
    classifier = 'sources'
}

artifacts {
    archives sourcesJar
}
uploadArchives {
    repositories {
        mavenDeployer {
            //deploy to maven repository
            if (debug_flag) {
                repository(url: uri('../repo-local')) //deploy to local warehouse
            } else {//deploy to maven private service
                def repoKey = maven_type_snapshot ? artifactory_snapshot_repoKey : artifactory_release_repoKey
                repository(url: "${artifactory_contextUrl}/${repoKey}") {
                    authentication(userName: artifactory_user, password: artifactory_password)
                }
            }

            pom.groupId = artifact_group
            pom.artifactId = artifact_id
            pom.version = artifact_version + (maven_type_snapshot ? '-SNAPSHOT' : '')

            pom.project {
                licenses {
                    license {
                        name 'The Apache Software License, Version 2.0'
                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
            }
        }
    }
}

Address and dependencies of the local repository to be added to the build.gradle file in the root directory

buildscript {

    repositories {
        maven{ url rootProject.file("repo-local") }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0-beta6'
        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'
        classpath 'com.billy.android:autoregister:1.0.1'
    }
}

2. Add class scan-related code to the Transform method of the Transform class

// Traverse input files
inputs.each { TransformInput input ->

    // Traversing jar
    input.jarInputs.each { JarInput jarInput ->
        String destName = jarInput.name
        // Duplicate name output file, because it may have the same name, will overwrite
        def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
        if (destName.endsWith(".jar")) {
            destName = destName.substring(0, destName.length() - 4)
        }
        // Get Input File
        File src = jarInput.file
        // Get Output File
        File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

        //Walk through jar's byte code class file to find component s that need to be registered automatically
        if (CodeScanProcessor.shouldProcessPreDexJar(src.absolutePath)) {
            CodeScanProcessor.scanJar(src, dest)
        }
        FileUtils.copyFile(src, dest)

        project.logger.info "Copying\t${src.absolutePath} \nto\t\t${dest.absolutePath}"
    }
    // List Folder Contents
    input.directoryInputs.each { DirectoryInput directoryInput ->
        // Catalog of products obtained
        File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        String root = directoryInput.file.absolutePath
        if (!root.endsWith(File.separator))
            root += File.separator
        //Traverse through each file in the directory
        directoryInput.file.eachFileRecurse { File file ->
            def path = file.absolutePath.replace(root, '')
            if(file.isFile()){
                CodeScanProcessor.checkInitClass(path, new File(dest.absolutePath + File.separator + path))
                if (CodeScanProcessor.shouldProcessClass(path)) {
                    CodeScanProcessor.scanClass(file)
                }
            }
        }
        project.logger.info "Copying\t${directoryInput.file.absolutePath} \nto\t\t${dest.absolutePath}"
        // Copy to target file after processing
        FileUtils.copyDirectory(directoryInput.file, dest)
    }
}

CodeScanProcessor is a tool class in which CodeScanProcessor.scanJar(src, dest) and CodeScanProcessor.scanClass(file) are used to scan jar packages and class files, respectively.
The principle of scanning is to use ASM's lassVisitor to view the parent class name of each class and the name of the interface it implements, compare it with the configuration information, and if it meets our filtering criteria, record it, and register it by calling the parameterless construction method of these classes after all scanning is complete

static void scanClass(InputStream inputStream) {
    ClassReader cr = new ClassReader(inputStream)
    ClassWriter cw = new ClassWriter(cr, 0)
    ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
    cr.accept(cv, ClassReader.EXPAND_FRAMES)
    inputStream.close()
}

static class ScanClassVisitor extends ClassVisitor {
    ScanClassVisitor(int api, ClassVisitor cv) {
        super(api, cv)
    }
    void visit(int version, int access, String name, String signature,
               String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
        RegisterTransform.infoList.each { ext ->
            if (shouldProcessThisClassForRegister(ext, name)) {
                if (superName != 'java/lang/Object' && !ext.superClassNames.isEmpty()) {
                    for (int i = 0; i < ext.superClassNames.size(); i++) {
                        if (ext.superClassNames.get(i) == superName) {
                            ext.classList.add(name)
                            return
                        }
                    }
                }
                if (ext.interfaceName && interfaces != null) {
                    interfaces.each { itName ->
                        if (itName == ext.interfaceName) {
                            ext.classList.add(name)
                        }
                    }
                }
            }
        }

    }
}

3. Record the file where the target class is located, because we will modify its byte code to insert the registration code

 static void checkInitClass(String entryName, File file) {
     if (entryName == null || !entryName.endsWith(".class"))
         return
     entryName = entryName.substring(0, entryName.lastIndexOf('.'))
     RegisterTransform.infoList.each { ext ->
         if (ext.initClassName == entryName)
             ext.fileContainsInitClass = file
     }
 }

4. After the scan is complete, start modifying the byte code of the target class (using the MethodVisitor of ASM to modify the static block of the target class, i.e. <clinit>method), and the generated code is a direct call to the lunch construction method of the scanned class, not a reflection

  • Class file: directly modify this byte code file (actually regenerate a class file and replace the original file)
  • Jar file: copy this jar file, find the JarEntry corresponding to the target class in the jar package, modify its byte code, and replace the original jar file
import org.apache.commons.io.IOUtils
import org.objectweb.asm.*

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
/**
 * @author billy.qi
 */
class CodeInsertProcessor {
    RegisterInfo extension

    private CodeInsertProcessor(RegisterInfo extension) {
        this.extension = extension
    }

    static void insertInitCodeTo(RegisterInfo extension) {
        if (extension != null && !extension.classList.isEmpty()) {
            CodeInsertProcessor processor = new CodeInsertProcessor(extension)
            File file = extension.fileContainsInitClass
            if (file.getName().endsWith('.jar'))
                processor.insertInitCodeIntoJarFile(file)
            else
                processor.insertInitCodeIntoClassFile(file)
        }
    }

    //Handling class code injection in jar packages
    private File insertInitCodeIntoJarFile(File jarFile) {
        if (jarFile) {
            def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
            if (optJar.exists())
                optJar.delete()
            def file = new JarFile(jarFile)
            Enumeration enumeration = file.entries()
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))

            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = file.getInputStream(jarEntry)
                jarOutputStream.putNextEntry(zipEntry)
                if (isInitClass(entryName)) {
                    println('codeInsertToClassName:' + entryName)
                    def bytes = referHackWhenInit(inputStream)
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                inputStream.close()
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            file.close()

            if (jarFile.exists()) {
                jarFile.delete()
            }
            optJar.renameTo(jarFile)
        }
        return jarFile
    }

    boolean isInitClass(String entryName) {
        if (entryName == null || !entryName.endsWith(".class"))
            return false
        if (extension.initClassName) {
            entryName = entryName.substring(0, entryName.lastIndexOf('.'))
            return extension.initClassName == entryName
        }
        return false
    }
    /**
     * Processing class injection
     * @param file class file
     * @return Modified byte code file contents
     */
    private byte[] insertInitCodeIntoClassFile(File file) {
        def optClass = new File(file.getParent(), file.name + ".opt")

        FileInputStream inputStream = new FileInputStream(file)
        FileOutputStream outputStream = new FileOutputStream(optClass)

        def bytes = referHackWhenInit(inputStream)
        outputStream.write(bytes)
        inputStream.close()
        outputStream.close()
        if (file.exists()) {
            file.delete()
        }
        optClass.renameTo(file)
        return bytes
    }


    //refer hack class when object init
    private byte[] referHackWhenInit(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream)
        ClassWriter cw = new ClassWriter(cr, 0)
        ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }

    class MyClassVisitor extends ClassVisitor {

        MyClassVisitor(int api, ClassVisitor cv) {
            super(api, cv)
        }

        @Override
        MethodVisitor visitMethod(int access, String name, String desc,
                                  String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
            if ("<clinit>" == name) { //Inject code into the static code block
                mv = new MyMethodVisitor(Opcodes.ASM5, mv)
            }
            return mv
        }
    }

    class MyMethodVisitor extends MethodVisitor {

        MyMethodVisitor(int api, MethodVisitor mv) {
            super(api, mv)
        }

        @Override
        void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                extension.classList.each { name ->
                    //Create a component instance with a parameterless construction method
                    mv.visitTypeInsn(Opcodes.NEW, name)
                    mv.visitInsn(Opcodes.DUP)
                    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, name, "<init>", "()V", false)
                    //Call the registration method to register the component instance into the component library
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC
                            , extension.registerClassName
                            , extension.registerMethodName
                            , "(L${extension.interfaceName};)V"
                            , false)
                }
            }
            super.visitInsn(opcode)
        }
        @Override
        void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack + 4, maxLocals)
        }
    }
}

5. Receive extended parameters to get the features of the class that needs to be scanned and the code that needs to be inserted

After finding a way for the gradle plug-in to receive custom object array extension parameters for a long time, it takes a step back and uses List<Map>Receive and then convert to receive extended parameters for multiple scan tasks

import org.gradle.api.Project
/**
 * aop Configuration information
 * @author billy.qi
 * @since 17/3/28 11:48
 */
class AutoRegisterConfig {

    public ArrayList<Map<String, Object>> registerInfo = []

    ArrayList<RegisterInfo> list = new ArrayList<>()

    Project project

    AutoRegisterConfig(){}

    void convertConfig() {
        registerInfo.each { map ->
            RegisterInfo info = new RegisterInfo()
            info.interfaceName = map.get('scanInterface')
            def superClasses = map.get('scanSuperClasses')
            if (!superClasses) {
                superClasses = new ArrayList<String>()
            } else if (superClasses instanceof String) {
                ArrayList<String> superList = new ArrayList<>()
                superList.add(superClasses)
                superClasses = superList
            }
            info.superClassNames = superClasses
            info.initClassName = map.get('codeInsertToClassName') //Class for Code Injection (insert registration code into the static block of this class)
            info.registerMethodName = map.get('registerMethodName') //Method invoked by generated code
            info.registerClassName = map.get('registerClassName') //The class where the registration method is located
            info.include = map.get('include')
            info.exclude = map.get('exclude')
            info.init()
            if (info.validate())
                list.add(info)
            else {
                project.logger.error('auto register config error: scanInterface, codeInsertToClassName and registerMethodName should not be null\n' + info.toString())
            }
        }
    }
}
import java.util.regex.Pattern
/**
 * @author billy.qi
 */
class RegisterInfo {
    static final DEFAULT_EXCLUDE = [
            '.*/R(\\$[^/]*)?'
            , '.*/BuildConfig$'
    ]
    //The following are configurable parameters
    String interfaceName = ''
    ArrayList<String> superClassNames = []
    String initClassName = ''
    String registerClassName = ''
    String registerMethodName = ''
    ArrayList<String> include = []
    ArrayList<String> exclude = []

    //The following are not configurable parameters
    ArrayList<Pattern> includePatterns = []
    ArrayList<Pattern> excludePatterns = []
    File fileContainsInitClass //Class file of initClassName or jar file containing initClassName class
    ArrayList<String> classList = new ArrayList<>()

    RegisterInfo(){}

    boolean validate() {
        return interfaceName && registerClassName && registerMethodName
    }

    void init() {
        if (include == null) include = new ArrayList<>()
        if (include.empty) include.add(".*") //If not set, default to include all
        if (exclude == null) exclude = new ArrayList<>()
        if (!registerClassName)
            registerClassName = initClassName

        //Convert'. 'in interfaceName to'/'
        if (interfaceName)
            interfaceName = convertDotToSlash(interfaceName)
        //Convert'. 'in superClassName to'/'
        if (superClassNames == null) superClassNames = new ArrayList<>()
        for (int i = 0; i < superClassNames.size(); i++) {
            def superClass = convertDotToSlash(superClassNames.get(i))
            superClassNames.set(i, superClass)
            if (!exclude.contains(superClass))
                exclude.add(superClass)
        }
        //interfaceName added to exclusion
        if (!exclude.contains(interfaceName))
            exclude.add(interfaceName)
        //Registration and initialization methods are in the same class by default
        initClassName = convertDotToSlash(initClassName)
        registerClassName = convertDotToSlash(registerClassName)
        //Add default exclusions
        DEFAULT_EXCLUDE.each { e ->
            if (!exclude.contains(e))
                exclude.add(e)
        }
        initPattern(include, includePatterns)
        initPattern(exclude, excludePatterns)
    }

    private static String convertDotToSlash(String str) {
        return str ? str.replaceAll('\\.', '/').intern() : str
    }

    private static void initPattern(ArrayList<String> list, ArrayList<Pattern> patterns) {
        list.each { s ->
            patterns.add(Pattern.compile(s))
        }
    }
}

Step 3: Configure the relevant extension parameters required to automatically register the plug-in in the application

Add an extended parameter to the build.gradle file of the main app module, for example:

//auto register extension
// Functional introduction:
//  Scan all classes that will be typed into the apk package at compile time
//  An implementation class of scanInterface or a subclass of scanSuperClasses
//  The following code is generated in the static block of the codeInsertToClassName class:
//  registerClassName.registerMethodName(scanInterface)
// Main points:
//  1. RegiserClassName.registerMethodName to be visible to codeInsertToClassName (public)
//  2. If registerClassName is not configured, registerClassName = codeInsertToClassName
//  3. The base class of abstract or the subinterface of scanInterface requires exclude
// Auto-generated code example:
/*
  In the com.billy.app_lib_interface.CategoryManager.class file
  static
  {
    register(new CategoryA()); //scanInterface Implementation Class
    register(new CategoryB()); //scanSuperClass Subclasses
  }
 */
apply plugin: 'auto-register'
autoregister {
    registerInfo = [
        [
            'scanInterface'             : 'com.billy.app_lib_interface.ICategory'
            , 'scanSuperClasses'        : ['com.billy.android.autoregister.demo.BaseCategory']
            , 'codeInsertToClassName'   : 'com.billy.app_lib_interface.CategoryManager'
            , 'registerMethodName'      : 'register'
            , 'exclude'                 : [//Excluded class that supports regular expressions (package separators need to be/represented, not.)
                'com.billy.android.autoregister.demo.BaseCategory'.replaceAll('\\.', '/') //Exclude this base class
            ]
        ],
        [
            'scanInterface'             : 'com.billy.app_lib.IOther'
            , 'codeInsertToClassName'   : 'com.billy.app_lib.OtherManager'
            , 'registerClassName'       : 'com.billy.app_lib.OtherManager'
            , 'registerMethodName'      : 'registerOther'
        ]
    ]
}

summary

This paper introduces the function of automatically scanning the implementation class of java interface and generating the registry gradle plug-in, and gives a brief explanation of its principle. It mainly introduces the implementation process of this plug-in, including the TransformAPI, ASM, groovy-related syntax, gradle mechanism.

All code for this plug-in and its demo are open source to github On, welcome fork, start

Next, use this plugin to automatically manage the registry for us!

For reprinting, please indicate the source: http://blog.csdn.net/cdecde111/article/details/78074692

Posted by AbiusX on Wed, 22 May 2019 09:16:47 -0700