APT(Annotation Processing Tool),根据注解自动给生成代码。
JavaPoet,代码生成框架。
要自动生成类文件,JavaPoet并不是必须的,比如JavaPoet的Example的一段代码,想要生成如下类文件:
package com.example.helloworld;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
不使用JavaPoet需要这样写:
JavaFileObject sourceFile = filer.createSourceFile(someFile);
Writer writer = sourceFile.openWriter();
writer.write("package com.example.helloworld;\n");
writer.write("public final class HelloWorld {\n");
writer.write("public static void main(String[] args) {\n");
writer.write("System.out.println(\"Hello, JavaPoet!\");\n");
writer.write("}\n");
writer.write("} \n");
writer.close();
使用JavaPoet是这样写的:
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(someFile);
回到组件化工程,首选要明确希望自动生成的类长什么样子,这里需要的是一个Activity的对应关系Map,比如Order模块的路由这样的:
public class Router$$Path$$app implements RouterLoadPath {
@Override
public Map<String, Class> loadPath() {
Map<String, Class> pathMap = new HashMap<>();
pathMap.put("MainActivity", MainActivity.class);
//该模块有几个Activity需要被其他模块访问,都要加进去
//pathMap.put(...);
//pathMap.put(...);
return pathMap;
}
}
RouterLoadPath接口是为了不需要知道具体类名就可以调用loadPath(),下面会在提到。
这样就可以根据模块名和类名拿到对应的Activity了。
明确目标之后,就要开始APT和JavaPoet的工作了:
- 新建Java Library:router_annotation,自定义注解Router:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS) // 要在编译时进行一些预处理操作,注解会在class文件中存在
public @interface Router {
// 详细路由路径,如:"MainActivity"
String path();
// 路由组名,如:"app"
String group();
}
- 新建注解处理器抹开Java Library:router_compiler,在build.gradle中需要导入三个库
- 谷歌提供的处理注解的服务库
- JavaPoet,用于自动生成代码的库
- 我们自定义的注解模块
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
// 帮助我们通过类调用的形式来生成Java代码
implementation "com.squareup:javapoet:1.9.0"
// 引入自定义annotation,处理@Router注解
implementation project(':router_annotation')
}
// java控制台输出中文乱码
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
sourceCompatibility = "7"
targetCompatibility = "7"
- 新建Router注解的处理类RouterProcessor,继承自AbstractProcessor:
// AutoService则是固定的写法,加个注解即可
@AutoService(Processor.class)
// 需要处理的注解
@SupportedAnnotationTypes({"com.yu.router_annotation.Router"})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数
@SupportedOptions({"moduleName", "packageNameForAPT"})
public class RouterProcessor extends AbstractProcessor {
// 该方法主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
...
}
// 处理注解的函数
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
...
return false;
}
}
这里的注解@ SupportedOptions要说明一下,这个是该处理器接收的参数,moduleName是模块名,packageNameForAPT是APT生成的文件存放包名,这个参数是在各模块的build.gradle中传递过来的,一会再看,先看init里需要初始化的工作,init的参数ProcessingEnvironment中可以获取一些工具类来处理注解:
// 操作Element工具类 (类、函数、属性都是Element)
Elements elementUtils = processingEnvironment.getElementUtils();
// type(类信息)工具类,包含用于操作TypeMirror的工具方法
Types typeUtils = processingEnvironment.getTypeUtils();
// Messager用来报告错误,警告和其他提示信息
Messager messager = processingEnvironment.getMessager();
// 文件生成器 类/资源,Filter用来创建新的类文件,class文件以及辅助文件
Filer filer = processingEnvironment.getFiler();
还需要通过ProcessingEnvironment把moduleName
和packageNameForAPT
两个参数获也取出来:
Map<String, String> options = processingEnvironment.getOptions();
moduleName = options.get("moduleName");
packageNameForAPT = options.get("packageNameForAPT");
然后就是process函数了,这里就是需要使用APT的工具,按照JavaPoet的规则去生成我们想要的类了,JavaPoet使用方法,看这里。
处理过程:
// 处理注解的函数
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 一旦有类之上使用@Router注解
if (!EmptyUtils.isEmpty(set)) {
// 获取所有被 @Router 注解的 元素集合
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Router.class);
if (!EmptyUtils.isEmpty(elements)) {
// 解析元素
try {
parseElements(elements);
return true;
} catch (IOException e) {
e.printStackTrace();
}
}
// 坑:必须写返回值,表示处理@Router注解完成
return true;
}
return false;
}
private void parseElements(Set<? extends Element> elements) throws IOException {
TypeName methodReturns = ParameterizedTypeName.get(
ClassName.get(Map.class), // Map
ClassName.get(String.class), // Map<String,
ClassName.get(Class.class) // Map<String, RouterBean>
);
MethodSpec.Builder methodBuidler = MethodSpec.methodBuilder("loadPath") // 方法名
.addAnnotation(Override.class) // 重写注解
.addModifiers(Modifier.PUBLIC) // public修饰符
.returns(methodReturns); // 方法返回值
// 初始化Map:Map<String, RouterBean> pathMap = new HashMap<>();
methodBuidler.addStatement("$T<$T, $T> $N = new $T<>()",
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(Class.class),
"pathMap",
HashMap.class);
// 便利Activity,存入Map
for (Element element : elements) {
Router router = element.getAnnotation(Router.class);
methodBuidler.addStatement(
"$N.put($S, $T.class)",
"pathMap",
router.path(), // "/app/MainActivity"
ClassName.get((TypeElement) element) // MainActivity.class
);
}
methodBuidler.addStatement("return $N", "pathMap");
// 最终生成的类文件名
String finalClassName = "router$$Path$$" + moduleName;
messager.printMessage(Diagnostic.Kind.NOTE, "APT生成路由Path类文件:" +
packageNameForAPT + "." + finalClassName);
// 生成类文件:Router$$Path$$app
JavaFile.builder(packageNameForAPT, // 包名
TypeSpec.classBuilder(finalClassName) // 类名
.addSuperinterface(ClassName.get(elementUtils.getTypeElement("com.yu.router_api.RouterLoadPath"))) // 实现RouterLoadPath接口
.addModifiers(Modifier.PUBLIC) // public修饰符
.addMethod(methodBuidler.build()) // 方法的构建(方法参数 + 方法体)
.build()) // 类构建完成
.build() // JavaFile构建完成
.writeTo(filer); // 文件生成器开始生成类文件
}
然后看下上看提到的两个参数,模块名和生成文件的包名,是在各模块的build.gradle中传递过来的,packageNameForAPT在公共的config.gradle中定义:
// 包名,用于存放APT生成的类文件
packageNameForAPT = "com.yu.modular.apt"
各模块的build.gradle中添加:
// 在gradle文件中配置选项参数值(用于APT传参接收)
// 切记:必须写在defaultConfig节点下
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName(), packageNameForAPT: packageNameForAPT]
}
}
生成的文件在对应模块的这个目录下,当然包名是自己定义的:
到这里APT自动生成的文件也搞完了,但是这里APT生成的Router\$\$Path\$\$模块名
的文件是在各自的模块中,在模块不互相引用的情况下不能直接获取,用Class.forName()去取当然没问题,这里在公共库里用一个Manager去管理,做了缓存和懒加载优化:
public class RouterManager {
private static final String TAG = "RouterManager";
private static RouterManager instance;
//Router$$Path$$xxx 类的缓存,防止每次调用Class.forName()、newInstance()
//结构如:
// {
// app : Router$$Path$$app对象,
// order : Router$$Path$$order对象
// ...
// }
private LruCache<String, RouterLoadPath> routerClassCache;
//目标类缓存,目前为 Activity
//结构如:
// {
// app/MainActivity : com.yu.modular.app.MainActivity.class,
// order/Order_MainActivity : com.yu.modular.order.Order_MainActivity.class
// ...
// }
private LruCache<String, Class> targetClassCache;
private RouterManager() {
routerClassCache = new LruCache<>(66);
targetClassCache = new LruCache<>(66);
}
/**
* 单例
*
* @return
*/
public static RouterManager getInstance() {
if (instance == null) {
synchronized (RouterManager.class) {
if (instance == null) {
instance = new RouterManager();
}
}
}
return instance;
}
/**
* 获取目标类
*
* @param groupName
* @param pathName
* @return
*/
public Class get(String groupName, String pathName) {
Class targetClass = targetClassCache.get(groupName + "/" + pathName);
if (targetClass == null) {
RouterLoadPath routerPathClass = getRouterPathClass(groupName);
Map<String, Class> pathMap = routerPathClass.loadPath();
targetClass = pathMap.get(pathName);
Log.e(TAG, "新建目标类文件:" + pathName);
targetClassCache.put(groupName + "/" + pathName, targetClass);
}
return targetClass;
}
/**
* 获取APT生成的类
*
* @param groupName
* @return
*/
public RouterLoadPath getRouterPathClass(String groupName) {
try {
RouterLoadPath routerLoadPath = routerClassCache.get(groupName);
if (routerLoadPath == null) {
String groupClassName = "com.yu.modular.apt.Router$$Path$$" + groupName;
routerLoadPath = (RouterLoadPath) Class.forName(groupClassName).newInstance();
Log.e(TAG, "新建RouterPath文件:" + groupName);
routerClassCache.put("groupName", routerLoadPath);
}
return routerLoadPath;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
可以愉快的跳转了了:
public void jumpOrder(View view) {
Class clazz = RouterManager.getInstance().get("order", "Order_MainActivity");
Intent intent = new Intent(this, clazz);
startActivity(intent);
}
还有一个问题,如果想跨模块访问数据怎么搞?
比如想要在shoppingCar模块中展示Order模块中的图片。当然这些图片资源可以放在公共库,假设这站图片就是属于shoppingCar业务的,只有Order要用一下,其他模块并不需要(也不要太较真,就是用图片做个例子,实现方式同样适用于传递Bean)。这个跟传递Activity原理是一样的,先看实现再分析为什么这么做。
首先在公共库中定义一个接口:
/**
* 订单模块对外暴露接口,其他模块可以获取返回res资源
* 具体的实现在Order模块中
*/
public interface OrderDrawable {
int getDrawable();
}
然后在Order模块中实现,并使用@Router注解:
@Router(group = "order", path = "OrderDrawable")
public class OrderDrawableImpl implements OrderDrawable {
@Override
public int getDrawable() {
return R.mipmap.order_wtf;
}
}
RouterManager封装一下:
/**
* 获取目标类对象
*
* @param groupName
* @param pathName
* @return
*/
public Object getResource(String groupName, String pathName) {
try {
// 调用获取Activity类的方法,返回newInstance()
return this.get(groupName, pathName).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
使用:
OrderDrawable orderDrawable = (OrderDrawable) RouterManager.getInstance().getResource("order", "OrderDrawable");
imageView.setImageResource(orderDrawable.getDrawable());
和Activity不一样的只有在公共库中新建了一个接口,为什么要创建这个接口?
因为两个模块是独立的,即使通过Class.forName("...").newInstance()获取到这个类,也不知道这个类有getDrawable()方法,所以需要公共库里的这个接口。
完事,只是一个实现的简单思路,很多不严谨的地方,比如判空、判断注解格式等等等等,需要封装抽取、优化、拓展还有很多很多空间。
项目地址