- 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"); } }