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