ARouter Source Code Adventures (I)

Keywords: Java Android Gradle

General programme

Here It's a blog about ARouter published by Yunxi Community. It's the general outline and guiding ideology of ARouter. But to be honest, if you don't touch the source code, it's really hard to understand the profound content of this blog, so I will reprint this blog after studying ARouter.

PS: Source code in Here

        

The code structure is as follows, where app is the only runnable demo.

Arounter-annotation stores defined annotations and two classes

RouteType is an enumeration type used to represent the type of route.

RouteMeta is actually a Bean that stores the basic information about route s, as you can see below.

aroute-api core API and processing code

The test-module-1 demo has nothing to do with the test content.

RouteProcessor

Starting with classes that are very clear about their roles, of course, they use Processor to automatically generate code. If you are not familiar with processors, you can refer to them. Here and Here.

First of all, the Init method (log part and some non-critical parts are replaced by...

public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFiler = processingEnv.getFiler();                  // Generate class.
        typeUtil = processingEnv.getTypeUtils();            // Get type utils.
        elementUtil = processingEnv.getElementUtils();      // Get class meta.
        logger = new Logger(processingEnv.getMessager());   // Package the log utils.

        // Attempt to get user configuration [moduleName]
        Map<String, String> options = processingEnv.getOptions();
        if (MapUtils.isNotEmpty(options)) {
            moduleName = options.get(KEY_MODULE_NAME);
        }

        if (StringUtils.isNotEmpty(moduleName)) {
            moduleName = moduleName.replaceAll("[^0-9a-zA-Z_]+", "");
            ...
        } else {
            ...
            throw new RuntimeException("ARouter::Compiler >>> No module name, for more information, look at gradle log.");
        }

        iProvider = elementUtil.getTypeElement(Consts.IPROVIDER).asType();

        ...
    }

In general, the code is relatively clear, and there are about three things to do.

1. Getting Tool Classes

2. Get the incoming moduleName parameter and remove the illegal characters (because the class name needs to be stitched) if no direct error is reported if the incoming module Name parameter is not passed in.

3. Get TypeMirror of the com.alibaba.android.arouter.facade.template.IProvider class. This kind of function will unveil the veil bit by bit in the follow-up study.

Then the process method:

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (CollectionUtils.isNotEmpty(annotations)) {
            Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
            try {
                logger.info(">>> Found routes, start... <<<");
                this.parseRoutes(routeElements);

            } catch (Exception e) {
                logger.error(e);
            }
            return true;
        }

        return false;
    }

This method actually identifies all the elements that are modified with Route, and then calls the parseRoutes method.

private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        if (CollectionUtils.isNotEmpty(routeElements)) {
            rootMap.clear();

            // Fantastic four
            TypeElement type_Activity = elementUtil.getTypeElement(ACTIVITY);
            TypeElement type_Service = elementUtil.getTypeElement(SERVICE);

            // Interface of ARouter.
            TypeElement type_IRouteGroup = elementUtil.getTypeElement(IROUTE_GROUP);
            TypeElement type_IProviderGroup = elementUtil.getTypeElement(IPROVIDER_GROUP);
            ClassName routeMetaCn = ClassName.get(RouteMeta.class);
            ClassName routeTypeCn = ClassName.get(RouteType.class);

            /*
               Build input type, format as :

               ```Map<String, Class<? extends IRouteGroup>>```
             */
            ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(
                    ClassName.get(Map.class),
                    ClassName.get(String.class),
                    ParameterizedTypeName.get(
                            ClassName.get(Class.class),
                            WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup))
                    )
            );

            /*

              ```Map<String, RouteMeta>```
             */
            ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
                    ClassName.get(Map.class),
                    ClassName.get(String.class),
                    ClassName.get(RouteMeta.class)
            );

            /*
              Build input param name.
             */
            ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build();
            ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
            ParameterSpec providerParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "providers").build();  // Ps. its param type same as groupParamSpec!

            /*
              Build method : 'loadInto'
             */
            MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                    .addAnnotation(Override.class)
                    .addModifiers(PUBLIC)
                    .addParameter(rootParamSpec);


            .......................
        }
    }

To read a paragraph, first get the TypeElement of native activity and service in android.

PS, if you are confused, you may need to follow. Here Go again.

Then get the TypeElement of com.alibaba.android.arouter.facade.template.IRouteGroup and com.alibaba.android.arouter.facade.template.IProviderGroup.

Then there are some pre-operations for building java files (using javapoet).

Parameters Map < String, Class <? Extends IRouteGroup > routes are created

Create parameters Map < String, RouteMeta > Atlas

Parameters Map < String, RouteMeta > providers were created

Then we create a method @Override public loadInto (Map < String, Class <? Extends IRouteGroup > routes) {} (temporarily called Method A) whose content has not yet been filled in.

private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        ............
            //  Follow a sequence, find out metas of group first, generate java file, then statistics them as root.
            for (Element element : routeElements) {
                TypeMirror tm = element.asType();
                Route route = element.getAnnotation(Route.class);
                RouteMeta routeMete = null;

                if (typeUtil.isSubtype(tm, type_Activity.asType())) {                 // Activity
                    logger.info(">>> Found activity route: " + tm.toString() + " <<<");

                    // Get all fields annotation by @Autowired
                    Map<String, Integer> paramsType = new HashMap<>();
                    for (Element field : element.getEnclosedElements()) {
                        if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !typeUtil.isSubtype(field.asType(), iProvider)) {
                            // It must be field, then it has annotation, but it not be provider.
                            Autowired paramConfig = field.getAnnotation(Autowired.class);
                            paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), TypeUtils.typeExchange(field.asType()));
                        }
                    }
                    routeMete = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
                } else if (typeUtil.isSubtype(tm, iProvider)) {         // IProvider
                    logger.info(">>> Found provider route: " + tm.toString() + " <<<");
                    routeMete = new RouteMeta(route, element, RouteType.PROVIDER, null);
                } else if (typeUtil.isSubtype(tm, type_Service.asType())) {           // Service
                    logger.info(">>> Found service route: " + tm.toString() + " <<<");
                    routeMete = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
                }

                categories(routeMete);
                // if (StringUtils.isEmpty(moduleName)) {   // Hasn't generate the module name.
                //     moduleName = ModuleUtils.generateModuleName(element, logger);
                // }
            }

          ........
    }

Circular traversal analysis of route-modified nodes, if it is a subclass of IProvider or a subclass of service, directly deposits the annotation type object Route, the Element of the annotated object, and the route type (see RouteType enumeration).

If the node is a subclass of activity, you need to get all the fields of non-IProvider type annotated by Autowire in this class (which should be used for injection, not looking at the source code, not yet clear). Store as map < field name, enumerated value of field type (see TypeUtils)> Note that the above Route classification will be frequently used later.

private void categories(RouteMeta routeMete) {
        if (routeVerify(routeMete)) {
            logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<");
            Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
            if (CollectionUtils.isEmpty(routeMetas)) {
                Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
                    @Override
                    public int compare(RouteMeta r1, RouteMeta r2) {
                        try {
                            return r1.getPath().compareTo(r2.getPath());
                        } catch (NullPointerException npe) {
                            logger.error(npe.getMessage());
                            return 0;
                        }
                    }
                });
                routeMetaSet.add(routeMete);
                groupMap.put(routeMete.getGroup(), routeMetaSet);
            } else {
                routeMetas.add(routeMete);
            }
        } else {
            logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<");
        }
    }

    /**
     * Verify the route meta
     *
     * @param meta raw meta
     */
    private boolean routeVerify(RouteMeta meta) {
        String path = meta.getPath();

        if (StringUtils.isEmpty(path) || !path.startsWith("/")) {   // The path must be start with '/' and not empty!
            return false;
        }

        if (StringUtils.isEmpty(meta.getGroup())) { // Use default group(the first word in path)
            try {
                String defaultGroup = path.substring(1, path.indexOf("/", 1));
                if (StringUtils.isEmpty(defaultGroup)) {
                    return false;
                }

                meta.setGroup(defaultGroup);
                return true;
            } catch (Exception e) {
                logger.error("Failed to extract default group! " + e.getMessage());
                return false;
            }
        }

        return true;
    }

PS: Consider it. Stick up the code. At least it doesn't seem empty. Laugh.

First, verify that RouteMeta, Route's path cannot be empty, and it needs to start with "/" and contain at least two "/". If no group is set, the first word of path (between the first two "/" is used as the default group by default.

Then grouped according to group, put into HashMap < String, Set < RouteMeta > groupMap. The same group is sorted by path in a dictionary and placed in a TreeSet.

Therefore, the above code is to traverse Element s of Route annotations, get basic information, and store it in groups.

For ease of understanding, the following code is somewhat interpolated, which may confuse you about the structure of the source code. Please combine your own understanding with the source code.

private void parseRoutes(Set<? extends Element> routeElements) throws IOException {

            ............

            MethodSpec.Builder loadIntoMethodOfProviderBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                    .addAnnotation(Override.class)
                    .addModifiers(PUBLIC)
                    .addParameter(providerParamSpec);

            // Start generate java source, structure is divided into upper and lower levels, used for demand initialization.
            for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
                String groupName = entry.getKey();

                ..........

                // Build group method body
                Set<RouteMeta> groupData = entry.getValue();
                for (RouteMeta routeMeta : groupData) {
                    switch (routeMeta.getType()) {
                        case PROVIDER:  // Need cache provider's super class
                            List<? extends TypeMirror> interfaces = ((TypeElement) routeMeta.getRawType()).getInterfaces();
                            for (TypeMirror tm : interfaces) {
                                if (typeUtil.isSubtype(tm, iProvider)) {
                                    // This interface extend the IProvider, so it can be used for mark provider
                                    loadIntoMethodOfProviderBuilder.addStatement(
                                            "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                                            tm.toString().substring(tm.toString().lastIndexOf(".") + 1),    // Spite unuseless name
                                            routeMetaCn,
                                            routeTypeCn,
                                            ClassName.get((TypeElement) routeMeta.getRawType()),
                                            routeMeta.getPath(),
                                            routeMeta.getGroup());
                                }
                            }
                            break;
                        default:
                            break;
                    }

                   ...............

                }

                ............

            }

            .........

            // Wirte provider into disk
            String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(providerMapFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(type_IProviderGroup))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfProviderBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

            ............
        }
    }

Create a method @Override public void loadInto (Map < String, RouteMeta > providers) {} temporarily called B

Then traverse each RouteMeta in the groupMap generated in the previous step, if the RoteMeta is used to record IProvider information (see above, the class annotated by Route is a subclass of IProvider), traverse all directly or indirectly inherited interfaces of the class, and if the interface is IProvider or its subclass (code X), then load Into B (no). It is the method A that was created initially. The following statement is added to the method A as follows:

Provders. put ("the class name of X", RouteMeta.build(RouteType.PROVIDER, the class annotated by Route, Route's path, Route's group, null, Route's priority, Route's extra));

The task of method B is to store all IProvide type classes annotated by Route into the incoming parameters according to the above statement.

Then a java file is generated, packaged as com.alibaba.android.arouter.routes class named ARouter $Providers $XXX (XXX is the parameter moduleName that was first passed in and processed), inheriting the IProviderGroup interface. There is only one method in this class, namely B.

Continue to analyze code snippets:

   private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
           
            ...................

            // Start generate java source, structure is divided into upper and lower levels, used for demand initialization.
            for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
                String groupName = entry.getKey();

                MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                        .addAnnotation(Override.class)
                        .addModifiers(PUBLIC)
                        .addParameter(groupParamSpec);

                // Build group method body
                Set<RouteMeta> groupData = entry.getValue();
                for (RouteMeta routeMeta : groupData) {
                    
                    ................

                    // Make map body for paramsType
                    StringBuilder mapBodyBuilder = new StringBuilder();
                    Map<String, Integer> paramsType = routeMeta.getParamsType();
                    if (MapUtils.isNotEmpty(paramsType)) {
                        for (Map.Entry<String, Integer> types : paramsType.entrySet()) {
                            mapBodyBuilder.append("put(\"").append(types.getKey()).append("\", ").append(types.getValue()).append("); ");
                        }
                    }
                    String mapBody = mapBodyBuilder.toString();

                    loadIntoMethodOfGroupBuilder.addStatement(
                            "atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                            routeMeta.getPath(),
                            routeMetaCn,
                            routeTypeCn,
                            ClassName.get((TypeElement) routeMeta.getRawType()),
                            routeMeta.getPath().toLowerCase(),
                            routeMeta.getGroup().toLowerCase());
                }

                // Generate groups
                String groupFileName = NAME_OF_GROUP + groupName;
                JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                        TypeSpec.classBuilder(groupFileName)
                                .addJavadoc(WARNING_TIPS)
                                .addSuperinterface(ClassName.get(type_IRouteGroup))
                                .addModifiers(PUBLIC)
                                .addMethod(loadIntoMethodOfGroupBuilder.build())
                                .build()
                ).build().writeTo(mFiler);

                logger.info(">>> Generated group: " + groupName + "<<<");
                rootMap.put(groupName, groupFileName);
            }

         .............

    }

Traversing through all groups in groupMap, creating method @Override public void loadInto (Map < String, RouteMeta > atlas) is temporarily referred to as C

Next, all RouteMates in the group are traversed, and each RouteMate generates the following statement to insert into method C

Atlas. put (Path of Route, RouteMeta. build (RouteMate type, annotation class, Path of Route, Group of Route, map of Autowire, priority of Route, extra of Route);

Where map is similar to the following

 

new java.util.HashMap<String, Integer>(){{put("name", 18); put("boy", 0); put("age", 3); put("url", 18); }}

Then a java file is generated for the group. The package name is com.alibaba.android.arouter.routes class is ARouter$Group$XXX (XXX is the groupName of the group), and the interface IRouteGroup is inherited. The method is only C.

Finally, you need to store groupName: the key-value pair of the class name generated by the group in the rootMap

Victory is just around the corner. Look at the last piece of code.

if (MapUtils.isNotEmpty(rootMap)) {
                // Generate root meta by group name, it must be generated before root, then I can findout the class of group.
                for (Map.Entry<String, String> entry : rootMap.entrySet()) {
                    loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
                }
            }

            .......

            // Write root meta into disk.
            String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(rootFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(elementUtil.getTypeElement(ITROUTE_ROOT)))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfRootBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

Do you remember method A? After the creation was left out for a long time, go back and look at it!!

Traverse the rootMap and add the following statement in Method A

routes.put(groupName, group corresponding class name.class);

Finally, a java file is generated. The package, like the previous generated file, is named ARouter $Root $XXX (xxx means moduleName), inherits the interface IRouteRoot, and adds method A!

summary

At this point, the content of RouteProcessor has been fully analyzed, and we know that the processor will generate some files. In the case of demo, it will generate the following files altogether

        

Self-generated classes in arouter-api module

        

PS: Except ARouter $Interceptors $app

Classes generated by app module

PS: Except ARouter $Interceptors $testmodule 1

Code generated by test-module-1 module

It's easy to understand the source code in terms of the generated code content.

PS: In fact, we may be able to understand what Processor does by looking directly at the generated files, but I still have a detailed record, the purpose is to improve ourselves through the source code imperceptibly, I hope so!

In the next article, we will continue our exploration in ARouter.

Posted by rachelk on Sun, 21 Apr 2019 22:09:36 -0700