Android Basics: implement a simple route using Annotation Processor

Keywords: Java Android

  • Only as personal study notes, please verify carefully.

1, Introduction to Annotation Processor

Annotation Processor means Annotation Processor; In the compilation stage of java code, the Annotation Processor will process annotations. In this mechanism, we can customize the Annotation Processor to realize different processing of annotations.

Annotation Processor is used in Dagger, butterknife and other open source libraries.

2, Background description

Hypothesis: a routing management class RouterManager() needs to be created to distribute different scheme s, that is, this class can receive a string parameter and jump to different logic according to the string parameter; How can we achieve it?

2.1 define the Router interface as follows:

public interface IRouter {
    void dispatch();
}

2.2 implementation of IRouter

public class CodeRouter implements IRouter {
    @Override
    public void dispatch() {
        Log.d(ROUTER_TAG, "CodeRouter");
    }
}

Or:

public class HttpRouter implements IRouter {
    @Override
    public void dispatch() {
        Log.d(ROUTER_TAG, "HttpRouter");
    }
}

2.3 create a RouterManager class

public class RouterManager {
    /**
    * key Is scheme, and value is the corresponding class name;
    */
    private HashMap<String, String> map = new HashMap<>();
    /**
    * key Yes, scheme value is the corresponding class example;
    */
    private HashMap<String, IRouter> routerMap = new HashMap<>();

    private static final class Host {
        private static final RouterManager instance = new RouterManager();
    }

    private RouterManager() {
    }

    /**
    * Singleton mode
    */
    public static RouterManager getInstance() {
        return Host.instance;
    }
    
    /**
    * Initialize Router list
    */
    public void initRouter() {
        RouterManager.getInstance().register("bc://code", "com.bc.router.CodeRouter");
        RouterManager.getInstance().register("bc://http", "com.bc.router.HttpRouter");
    }

    /**
    * Register Router
    */
    public void register(String uri, String className) {
        if (className != null && uri != null) {
            map.put(uri, className);
            routerMap.put(uri, null);
        }
    }

    /**
    * Output all routers
    */
    public void showAllScheme() {
        System.out.println("RouterManager:" + map.toString());
    }

    /**
    * Execute different distribution logic according to scheme
    */
    public boolean dispatch(String scheme) {
        try {
            if (routerMap.containsKey(scheme)) {
                IRouter router = routerMap.get(scheme);
                if (router == null) {
                    router = (IRouter) Class.forName(map.get(scheme)).newInstance();
                    routerMap.put(scheme, router);
                }
                router.dispatch();
                return true;
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return false;
    }
}

2.4 initialization and use

class Activity{
    
    public void onCreate() {
        RouterManager.getInstance().initRouter();
    }
    
    public void onClick() {
        RouterManager.getInstance().dispatch("bc://code");
    }
}

The disadvantage of this implementation is that every time the new Router is added, a registered register method is called in RouterManager.initRouter. When there are multiple modules, this method is not flexible enough. Next, the implementation method of annotation + Annotation Processor is used.

3, Implementation method of Annotation Processor

(1) The new Java Library module is used to store annotation and basic RouterManager.

(2) Create a new Java Library module lib_compiler is used for AnnotationProcessor annotation processing.

3.1 custom annotation RouterProvider

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface RouterProvider {
    public String uri() default "";
}

3.2 add annotation on IRouter implementation class

@RouterProvider(uri = "bc://code")
public class CodeRouter implements IRouter {
    @Override
    public void dispatch() {
        Log.d(ROUTER_TAG, "CodeRouter");
    }
}

perhaps

@RouterProvider(uri = "bc://http")
public class HttpRouter implements IRouter {
    @Override
    public void dispatch() {
        Log.d(ROUTER_TAG, "HttpRouter");
    }
}

3.3 create a RouterManager

The only difference from the previous implementation is that initRouter(), which calls a TestRouterInit.initRouter() method to realize initialization. TestRouterInit and its initRouter() method are dynamically generated in the later Annotation Processor process. The definition is as follows:

public class RouterManager {

    public static final String INIT_CLASS = "com.bc.router.TestRouterInit";
    public static final String INIT_PACKAGE = "com.bc.router";
    public static final String INIT_SIMPLE_CLASS = "TestRouterInit";
    public static final String INIT_METHOD = "initRouter";
    public static final String ROUTER_TAG = "router";
    
    /**
    * key Is scheme, and value is the corresponding class name;
    */
    private HashMap<String, String> map = new HashMap<>();
    /**
    * key Yes, scheme value is the corresponding class example;
    */
    private HashMap<String, IRouter> routerMap = new HashMap<>();

    private static final class Host {
        private static final RouterManager instance = new RouterManager();
    }

    private RouterManager() {
    }

    /**
    * Singleton mode
    */
    public static RouterManager getInstance() {
        return Host.instance;
    }
    
    /**
    * Initialize the Router list: the only difference
    */
    public void initRouter() {
        try {
            //Call dynamically generated file
            Class.forName(INIT_CLASS).getMethod(INIT_METHOD).invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
    * Register Router
    */
    public void register(String uri, String className) {
        if (className != null && uri != null) {
            map.put(uri, className);
            routerMap.put(uri, null);
        }
    }

    /**
    * Output all routers
    */
    public void showAllScheme() {
        System.out.println("RouterManager:" + map.toString());
    }

    /**
    * Execute different distribution logic according to scheme
    */
    public boolean dispatch(String scheme) {
        try {
            if (routerMap.containsKey(scheme)) {
                IRouter router = routerMap.get(scheme);
                if (router == null) {
                    router = (IRouter) Class.forName(map.get(scheme)).newInstance();
                    routerMap.put(scheme, router);
                }
                router.dispatch();
                return true;
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return false;
    }
}

3.4 create a new Annotation Processor module

Create a new Annotation Processor module lib_compiler, the module needs to rely on auto service and javapoet, and its build.gradle file is as follows:

apply plugin: 'java-library'
apply plugin: 'kotlin'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    // auto-service
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
    // javapoet
    implementation 'com.squareup:javapoet:1.11.1'
    // This module stores the annotations we need to process
    implementation project(path: ":annotation")
}

Of which:

(1) Auto Service: AutoService is an open source library provided by Google to facilitate the generation of open source libraries conforming to the ServiceLoader specification

(2) Javapool: javapool is an open source java code generation framework launched by square, which provides Java Api to generate. Java source files.

3.5 customized Annotation Processor

Inherit AbstractProcessor, customize Annotation Processor, and process annotations:

@AutoService(Processor.class)
public class TestRouterProcessor extends AbstractProcessor {

    public static final String ROOT_INIT = RouterManager.INIT_PACKAGE;
    public static final String INIT_CLASS = RouterManager.INIT_SIMPLE_CLASS;
    public static final String INIT_METHOD = RouterManager.INIT_METHOD;

    @Override public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    //This method is very necessary, otherwise it will not execute to the process() method
    @Override public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(RouterProvider.class.getCanonicalName());
    }

    @Override public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        if (annotations == null || annotations.isEmpty()) {
            return false;
        }
        try {
            //Use javapoet to dynamically generate code: initialize function init()
            MethodSpec.Builder mainMethodBuilder = MethodSpec.methodBuilder(INIT_METHOD)
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(void.class);
            for (Element elementItem : env.getElementsAnnotatedWith(RouterProvider.class)) {
                if (!(elementItem instanceof TypeElement)) {
                    continue;
                }
                //Get the content in the annotation
                TypeElement element = (TypeElement) elementItem;
                String className = element.getQualifiedName().toString();
                String uri = element.getAnnotation(RouterProvider.class).uri();
               // The code in the method calls the register() method mainMethodBuilder.addStatement("$T.getInstance().register($S,$S)", RouterManager.class, uri, className) one by one on the annotation;
            }
            //Use javapoet to dynamically generate code: initialization class com.bc.router.TestRouterInit
            TypeSpec testRouterInit = TypeSpec.classBuilder(INIT_CLASS)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(mainMethodBuilder.build())
                    .build();
            JavaFile javaFile = JavaFile.builder(ROOT_INIT, testRouterInit)
                    .build();
            Filer filer = processingEnv.getFiler();
            javaFile.writeTo(filer);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
}

The above customized annotation processing process finally generates a class TestRouterInit:

public class TestRouterInit() {
    public static void initRouter() {
        RouterManager.getInstance().register("bc://code", "com.bc.router.CodeRouter");
        RouterManager.getInstance().register("bc://http", "com.bc.router.HttpRouter");
    }
}

3.6 rely on custom Annotation Processor

annotationProcessor project(":lib_compiler")

After the annotation processor is added under the module, the customized annotation processor will process the annotations during compilation.

3.7 initialization and use

ditto;

class Activity{
    
    public void onCreate() {
        RouterManager.getInstance().initRouter();
    }
    
    public void onClick() {
        RouterManager.getInstance().dispatch("bc://code");
    }
}

Posted by PHPThorsten on Wed, 06 Oct 2021 16:15:29 -0700