Component architecture design: routing architecture design and coding implementation

Keywords: Android Java JDK Gradle

Blog Homepage

Design of component-based routing architecture

In the previous article, we explained the class loading and global Map recording to realize the interaction between component modules, and slowly derived the APT technology.

So in the component architecture, we need to think about what kind of class files are generated through apt + javapot technology?

From the perspective of component-based routing architecture, we need to think about why it is necessary to use APT to generate files in groups? What is the purpose of generating these files?

Design idea: in initialization, only the packet table data is loaded. When using a packet, all routing information under a packet will be loaded instead of B packet data. For example, group A has 100 routing information and group B has 200 routing information. If you do not group, you need to load 300 routing information in the Map. When you do not need to enter the page of group B at all, loading the routing information of group B will lead to a waste of memory and time.

Todo'u modular project: using routing componentization project

  • app: main project (application)
  • common: basic module (library)
  • Shop: shopping shop function module
  • Personal: my personal function module

Routing framework

  • annotations (java library): annotation module, custom annotation for api and compiler dependency
  • compiler: the APT module (java library) generates java files at compile time
  • api: core module, the project needs to introduce the dependent module, using the java class generated by the compiler module to complete the creation of the routing table

Design Coding implementation

Routemetaroute information encapsulation class, route address, route group name, original class element, etc.

import javax.lang.model.element.Element;
public class RouteMeta {

    public enum RouteType {
        ACTIVITY
    }

    // Routing type, supporting Activity
    private RouteType type;
    // Primitive class elements
    private Element element;
    // Class object used by annotation
    private Class<?> clazz;
    // Routing address
    private String path;
    // Routing group name
    private String group;
}

Thinking: why do Element nodes need to be saved?

Because in the annotation processor, each class node is retrieved circularly, which is convenient for assignment and call. Element is under the javax.lang package and does not belong to Android Library.

IRouteRoot interface

Map<String, Class<? extends IRouteGroup>>
key: group name, such as: app, value: all path file names under the group name, such as: ARouter$$Group$$app.class

Note: after the route path file is generated, ARouter$$Group$$app.class is required to generate the route group file. The interface mode is easy to expand

/**
 * Routing group load data interface
 */
public interface IRouteRoot {

    /**
     *  Load routing group data
     *  For example: key: "app", value: arouter $$group $$app.class (implemented IRouteGroup interface)
     */
    void loadInto(Map<String, Class<? extends IRouteGroup>> routes);
}

IRouteGroup interface

Map<String, RouteMeta>
key: path name, such as "/ app/MainActivity", value: the corresponding route information under the path name

OOP idea makes the simple targetClass become a more flexible routemetaobject, and the interface mode is easy to expand

/**
 * Detailed Path path loading data interface corresponding to routing group
 * For example, which classes need to be loaded under the app group
 */
public interface IRouteGroup {

    /**
     *  Load Path details in routing group
     *  For example: key: "/ app/MainActivity", value: MainActivity information is encapsulated in RouteMeta object
     */
    void loadInto(Map<String, RouteMeta> atlas);
}

ARouter$$Group$$XX and ARouter$$Root$$XX file generation

Take the shopping module shop as an example to generate ARouter$$Group$$shop and ARouter$$Root$$shop files. The technical points involved are: apt + javapot

Before using apt + javapot to generate the code we want, we should first design the code template we want, such as the template code grouped by ARouter$$Root$$shop

public final class ARouter$$Root$$shop implements IRouteRoot {
  @Override
  public void loadInto(final Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("shop", ARouter$$Group$$shop.class);;
  }
}

ARouter$$Group$$shop all routing information code templates under this group

public class ARouter$$Group$$shop implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/shop/ShopActivity", RouteMeta.build(RouteMeta.RouteType.ACTIVITY, ShopActivity.class, "/shop/ShopActivity", "shop"));;
    atlas.put("/shop/ShopDetailActivity", RouteMeta.build(RouteMeta.RouteType.ACTIVITY, ShopDetailActivity.class, "/shop/ShopDetailActivity", "shop"));;
  }
}

In order to generate the above template code, the first @ Router annotation is used. To implement Activity jump, you need to use this annotation to mark Activity, process these annotations through annotation processor, and generate code in compiler:

@Target(ElementType.TYPE) // This annotation acts on the class
@Retention(RetentionPolicy.CLASS) // Do some preprocessing at compile time. The annotation will exist in the class file
public @interface Router {

    // Detailed route path (required), such as: "app/MainActivity"
    String path();

    // Routing group name (optional, if not filled in, it can be intercepted from path)
    String group() default "";
}

Then create a routing annotation processor, named ARouterProcessor, which inherits AbstractProcessor. The annotation processor needs to be registered. With the help of AutoService, we can automatically register to META-INF. At the same time, you need to specify the annotation type supported by the annotation processor, the version compiled by JDK, and the parameters received by the annotation processor.

// Allowed / supported annotation types for annotation processor to handle (new annotation module)
@SupportedAnnotationTypes({"com.example.modular.annotations.Router"})
// Specify JDK compiled version
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// Parameters received by annotation processor
@SupportedOptions({"moduleName"})
// AutoService is a fixed way of writing. Just add an annotation
// Through @ auto service in auto service, an AutoService annotation processor can be automatically generated for registration
// Used to generate META-INF/services/javax.annotation.processing.Processor file
@AutoService(Processor.class)
public class ARouterProcessor extends AbstractProcessor { }

Next, we implement the process abstract method, which is equivalent to the main method, which annotates the entry method of the processor to process the elements marked by @ Router.

  /**
 * Equivalent to main function, start processing annotation
 * The core method of annotation processor, dealing with specific annotations and generating Java files
 *
 * @param set              Support the collection of annotation types, such as @ ARouter annotation
 * @param roundEnvironment The current or previous running environment, through which you can find the annotations
 * @return true Indicates that the subsequent processor will no longer process (processing completed)
 */
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // Set set is the set of supported annotations, such as ARouter annotation
    if (set.isEmpty()) return false;

    // Get all elements annotated by @ ARouter annotation
    Set<? extends Element> elementsAnnotatedWithARouter = roundEnvironment.getElementsAnnotatedWith(Router.class);

    if (elementsAnnotatedWithARouter != null && !elementsAnnotatedWithARouter.isEmpty()) {

        try {
            parseElements(elementsAnnotatedWithARouter);
        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.NOTE, "Exception occurred:" + e.getMessage());
        }
    }
    return true;
}

Before processing the elements marked by @ Router annotation, you need to initialize some tools, override the init method of AbstractProcessor class, and obtain auxiliary tools through processingEnvironment, such as the tools for printing Log, file generation tools, etc.

// The tool class that operates the Element (for example, the class, function and property are all Element)
private Elements elementUtils;

// Messager is used to report errors, warnings and other prompt messages
private Messager messager;

// Type (class information) tool class, containing the tool methods used to operate TypeMirror
private Types typeUtils;

// File generator, class / resource, Filter to create new source files, class files and auxiliary files
private Filer filer;

// Module name, get build.gradle from getOptions and pass it on
private String moduleName;

// Cache route information, key: group name value: all route information under the group name
private Map<String, List<RouteMeta>> cacheRouteMetaMap = new HashMap<>();

// Cache group information, key: group name value: the corresponding routing class name under the group name, such as: Router$$Group$$shop
private Map<String, String> cacheGroupMap = new HashMap<>();

/**
 * This method is mainly used for some initialization work. Some tools can be obtained through the processingEnvironment parameter of this method
 */
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);

    elementUtils = processingEnvironment.getElementUtils();
    messager = processingEnvironment.getMessager();
    typeUtils = processingEnvironment.getTypeUtils();
    filer = processingEnvironment.getFiler();

    messager.printMessage(Diagnostic.Kind.NOTE, "<<<<< ARouterProcessor::init >>>>>");

    Map<String, String> options = processingEnvironment.getOptions();
    if (options != null && !options.isEmpty()) {
        moduleName = options.get("moduleName");
        messager.printMessage(Diagnostic.Kind.NOTE, "moduleName=" + moduleName);
    }

    if (Utils.isEmpty(moduleName)) {
        throw new RuntimeException("ARouter Annotation handler passed in moduleName Parameter cannot be empty, please check build.gradle Whether the profile configuration is correct");
    }
}

Resolve all the elements marked by @ Router annotation, encapsulate the path information in the satisfied condition elements into routemata and cache it. Used to generate group files and routing files under the group.

private void parseElements(Set<? extends Element> elements) throws IOException {

    TypeElement activityElement = elementUtils.getTypeElement("android.app.Activity");
    TypeMirror activityMirror = activityElement.asType();

    for (Element element : elements) {
        TypeMirror elementMirror = element.asType();
        messager.printMessage(Diagnostic.Kind.NOTE, "Traverse element information:" + elementMirror.toString());
        // Traversal element information: com.example.modular.shop.ShopActivity

        // Obtain the package node through the class node (full path name, such as: com.example.modular.shop)
        String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();

        // Get the simple class name annotated by @ ARouter
        String simpleName = element.getSimpleName().toString();

        // Note: Package Name: com.example.modular.shop annotated class name: ShopActivity
        messager.printMessage(Diagnostic.Kind.NOTE, "Package name:" + packageName + " Annotated class name:" + simpleName);


        Router aRouter = element.getAnnotation(Router.class);

        RouteMeta routeMeta = new RouteMeta(element, aRouter.group(), aRouter.path());

        // Determine whether the first type is a subtype of the second type, and return true if it is
        if (typeUtils.isSubtype(elementMirror, activityMirror)) {
            // Satisfied condition: the element annotated by @ ARouter is a subtype of Activity
            routeMeta.setType(RouteMeta.RouteType.ACTIVITY);
        } else {
            throw new RuntimeException("@ARouter Annotation can only be applied to Activity Class");
        }

        fillMapWithRouteMeta(routeMeta);

    }

    createGroupFile();

    createRootFile();
}

Fillmapwithroutemetamethod is used to cache route information

// Cache group information, key: group name value: the corresponding routing class name under the group name, such as: Router$$Group$$shop
private Map<String, String> cacheGroupMap = new HashMap<>();

private void fillMapWithRouteMeta(RouteMeta routeMeta) {
    if (checkRouterPath(routeMeta)) {
        messager.printMessage(Diagnostic.Kind.NOTE, "routeMeta>>>> " + routeMeta.toString());

        List<RouteMeta> routeMetas = cacheRouteMetaMap.get(routeMeta.getGroup());

        if (routeMetas == null) {
            routeMetas = new ArrayList<>();
            cacheRouteMetaMap.put(routeMeta.getGroup(), routeMetas);
        }
        routeMetas.add(routeMeta);
    } else {
        messager.printMessage(Diagnostic.Kind.NOTE, "routeMeta Check failed, specification not met");
    }
}

First, create a route file. The specific implementation of createGroupFile is as follows:

private void createGroupFile() throws IOException {
    if (cacheRouteMetaMap.isEmpty()) return;

    TypeElement routeGroupElement = elementUtils.getTypeElement("com.example.modular.api.IRouteGroup");

    for (Map.Entry<String, List<RouteMeta>> entry : cacheRouteMetaMap.entrySet()) {

        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ClassName.get(RouteMeta.class)
        );

        // Map<String, RouteMeta> atlas
        ParameterSpec atlasParameter = ParameterSpec.builder(parameterizedTypeName, "atlas").build();

        // public void loadInto(Map<String, RouteMeta> atlas)
        MethodSpec.Builder loadIntoMethodBuilder = MethodSpec.methodBuilder("loadInto")
                .addAnnotation(Override.class)
                .addParameter(atlasParameter)
                .addModifiers(Modifier.PUBLIC);


        List<RouteMeta> routeMetas = entry.getValue();
        for (RouteMeta routeMeta : routeMetas) {
            // atlas.put("/shop/ShopActivity", RouteMeta.build(RouteMeta.RouteType.ACTIVITY, ShopActivity.class, "/shop/ShopActivity", "shop"));
            loadIntoMethodBuilder.addStatement(
                    "$N.put($S, $T.build($T.$L, $T.class, $S, $S));",
                    "atlas",
                    routeMeta.getPath(),
                    ClassName.get(RouteMeta.class),
                    ClassName.get(RouteMeta.RouteType.class),
                    routeMeta.getType(),
                    ClassName.get((TypeElement) routeMeta.getElement()),
                    routeMeta.getPath(),
                    routeMeta.getGroup()
            );
        }

        MethodSpec loadIntoMethod = loadIntoMethodBuilder.build();

        String groupName = entry.getKey();

        String packageName = "com.example.android.arouter.routers";
        String finalClassName = "ARouter$$Group$$" + groupName;
        messager.printMessage(Diagnostic.Kind.NOTE, "Filename of the final generated path:" + packageName + "." + finalClassName);

        // public class Router$$Group$$shop implements IRouteGroup
        TypeSpec finalClass = TypeSpec.classBuilder(finalClassName)
                .addMethod(loadIntoMethod)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ClassName.get(routeGroupElement))
                .build();

        // Using file generator to generate files
        JavaFile.builder(packageName, finalClass).build().writeTo(filer);

        cacheGroupMap.put(groupName, finalClassName);
    }
}

Then create a group file. The createRootFile method is implemented as follows:

private void createRootFile() throws IOException {
    if (cacheGroupMap.isEmpty()) return;

    TypeElement routeRootElement = elementUtils.getTypeElement("com.example.modular.api.IRouteRoot");
    TypeElement routeGroupElement = elementUtils.getTypeElement("com.example.modular.api.IRouteGroup");

    // Map<String, Class<? extends IRouteGroup>>
    ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
            ClassName.get(Map.class),
            ClassName.get(String.class),
            ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(routeGroupElement)))
    );

    // Map<String, Class<? extends IRouteGroup>> routes
    ParameterSpec routesParameter = ParameterSpec.builder(parameterizedTypeName, "routes", Modifier.FINAL).build();

    // public void loadInto(Map<String, Class<? extends IRouteGroup>> routes)
    MethodSpec.Builder loadIntoMethodBuilder = MethodSpec.methodBuilder("loadInto")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(routesParameter);

    String packageName = "com.example.android.arouter.routers";

    for (Map.Entry<String, String> entry : cacheGroupMap.entrySet()) {

        // Such as: shop
        String groupName = entry.getKey();
        // For example: Router$$Group$$shop
        String finalGroupClassName = entry.getValue();

        // routes.put("shop", Router$$Group$$shop.class);
        loadIntoMethodBuilder.addStatement(
                "$N.put($S, $T.class);",
                "routes",
                groupName,
                ClassName.get(packageName, finalGroupClassName)
        );
    }

    MethodSpec loadIntoMethod = loadIntoMethodBuilder.build();

    String finalClassName = "ARouter$$Root$$" + moduleName;
    messager.printMessage(Diagnostic.Kind.NOTE, "Final generated group file name:" + packageName + "." +finalClassName);

    // public final class Router$$Root$$shop implements IRouteRoot
    TypeSpec finalClass = TypeSpec.classBuilder(finalClassName)
            .addMethod(loadIntoMethod)
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addSuperinterface(ClassName.get(routeRootElement)).build();

    JavaFile.builder(packageName, finalClass).build().writeTo(filer);
}

XX$$ARouter$$Parameter

For example, Activity jump needs to carry parameters. We need to get parameters through Activity.getIntent(). These template codes can also be generated by annotation processor.

public final class ShopActivity$$ARouter$$Parameter implements IParameter {
  @Override
  public void inject(final Object object) {
    ShopActivity target = (ShopActivity) object;;
    target.name = target.getIntent().getStringExtra("name");;
    target.age = target.getIntent().getIntExtra("shopAge", target.age);;
    target.isOpen = target.getIntent().getBooleanExtra("isOpen", target.isOpen);;
  }
}

Create a ParameterProcessor annotation processor to generate template code to get parameters. This class also inherits AbstractProcessor

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Parameter {
    String name() default "";
}

// Allowed / supported annotation types for annotation processor to handle (new annotation module)
@SupportedAnnotationTypes({"com.example.modular.annotations.Parameter"})
// Specify JDK compiled version
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// Parameters received by annotation processor
@SupportedOptions({"moduleName"})
// AutoService is a fixed way of writing. Just add an annotation
// Through @ auto service in auto service, an AutoService annotation processor can be automatically generated for registration
// Used to generate META-INF/services/javax.annotation.processing.Processor file
@AutoService(Processor.class)
public class ParameterProcessor extends AbstractProcessor {}

Implement the process method to process the elements marked by @ Parameter.

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    if (set.isEmpty()) return false;

    Set<? extends Element> elementsAnnotatedWithParameter = roundEnvironment.getElementsAnnotatedWith(Parameter.class);

    if (elementsAnnotatedWithParameter != null && !elementsAnnotatedWithParameter.isEmpty()) {

        fillMapWithElement(elementsAnnotatedWithParameter);
        try {
            createParameterFile();
        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.NOTE,
                    "ParameterProcessor Exception occurred:" + e.getMessage());
        }

        return true;
    }
    return false;
}

Where fillMapWithElement is used to cache all elements that meet the conditions

private final Map<TypeElement, List<Element>> cacheMap = new HashMap<>();

private void fillMapWithElement(Set<? extends Element> elementsAnnotatedWithParameter) {
    for (Element element : elementsAnnotatedWithParameter) {
        // @Parameter annotation acts on the field to get the parent element of the field
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        messager.printMessage(Diagnostic.Kind.NOTE,
                "@Parameter Traverse parent information:" + enclosingElement.getSimpleName() + " && Traversal element information: " + element.getSimpleName());

        List<Element> elements = cacheMap.get(enclosingElement);
        if (elements == null) {
            List<Element> fields = new ArrayList<>();
            fields.add(element);
            cacheMap.put(enclosingElement, fields);
        } else {
            elements.add(element);
        }
    }
}

Next use to create the build file

private void createParameterFile() throws IOException {
    if (cacheMap.isEmpty()) return;

    TypeElement activityElement = elementUtils.getTypeElement("android.app.Activity");

    ParameterSpec objectParameter = ParameterSpec.builder(TypeName.OBJECT, "object", Modifier.FINAL).build();

    TypeElement parameterType = elementUtils.getTypeElement("com.example.modular.api.IParameter");

    for (Map.Entry<TypeElement, List<Element>> entry : cacheMap.entrySet()) {

        TypeElement typeElement = entry.getKey();

        if (!typeUtils.isSubtype(typeElement.asType(), activityElement.asType())) {
            throw new RuntimeException("@Parameter Annotations currently only apply to Activity upper");
        }

        ClassName className = ClassName.get(typeElement);

        // public void inject(Object object)
        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(objectParameter);

        // MainActivity target = (MainActivity) object;
        injectMethodBuilder.addStatement(
                "$T target = ($T) $N;",
                className,
                className,
                "object"
        );

        List<Element> elements = entry.getValue();
        for (Element element : elements) {
            TypeMirror typeMirror = element.asType();

            int type = typeMirror.getKind().ordinal();

            String fieldName = element.getSimpleName().toString();

            // Get the field name defined in the annotation
            String name = element.getAnnotation(Parameter.class).name();

            // If not defined in the annotation, the default field name is used
            String finalName = Utils.isEmpty(name) ? fieldName : name;

            String finalValue = "target." + fieldName;

            String format = finalValue + " = target.getIntent().";

            if (type == TypeKind.INT.ordinal()) {
                // target.age = target.getIntent().getIntExtra("appAge", target.age);
                format += "getIntExtra($S, " + finalValue + ");";
            } else if (type == TypeKind.BOOLEAN.ordinal()) {
                // target.isOpen = target.getIntent().getBooleanExtra("isOpen", target.isOpen);
                format += "getBooleanExtra($S, " + finalValue + ");";
            } else {

                if (typeMirror.toString().equals("java.lang.String")) {
                    // target.name = target.getIntent().getStringExtra("name");
                    format += "getStringExtra($S);";
                }
            }

            if (format.endsWith(";")) {
                // target.name = target.getIntent().getStringExtra("name");
                injectMethodBuilder.addStatement(format, finalName);
            } else {
                messager.printMessage(Diagnostic.Kind.ERROR, "Currently only supported String int Boolean Type parameter");
            }
        }

        MethodSpec injectMethod = injectMethodBuilder.build();

        String finalClassName = typeElement.getSimpleName() + "$$ARouter$$Parameter";
        messager.printMessage(Diagnostic.Kind.NOTE,
                "Parameter files of final production:" + className.packageName() + "." + finalClassName);

        // public final class MainActivity$$Router$$Parameter implements IParameter
        TypeSpec finalClass = TypeSpec.classBuilder(finalClassName)
                .addMethod(injectMethod)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(ClassName.get(parameterType))
                .build();

        JavaFile.builder(className.packageName(), finalClass).build().writeTo(filer);
    }
}

If my article is helpful to you, please give me a compliment

Posted by hyngvesson on Fri, 13 Dec 2019 03:04:10 -0800