找了很久网上也没讲编译期注解的视频,只能对着网上代码一句句研究总结,主要学完这块,路由框架啊,奶油刀啊,基本自己就可以写点low版本的了
@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {}
上面是路由框架的注解,这个注解有什么用呢,路由框架会在项目的编译期通过注解处理器扫描所有添加@Route注解的Activity类,然后将Route注解中的path地址和Activity.class文件映射关系保存到它自己生成的java文件中,只要拿到了映射关系便能拿到Activity.class。相当于有一个类专门去保存了这些类和路径的关系。
好吧,我们先去学习下怎么搞个编译期注解
注解篇可以看我之前的基本注解讲解 :https://www.jianshu.com/p/e59059a509f1
我这里就直接上手了,其实核心原理就是我们扫描自己自定义的注解,然后根据注解拿到被我们注解的类的相关信息,保存生成一个我们的类,类里写上我们需要的东西,然后我们就可以根据这个类去搞一些事情。
大家也可以参考这篇博客,我是直接搞一下当自己日记了
https://blog.csdn.net/yang_yang1994/article/details/79729621
虚处理器AbstractProcessor
我们首先看一下处理器的API。每一个处理器都是继承于AbstractProcessor,如下所示:
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
先进行参数讲解,也会贴一些api上来,不然后面没办法用。
- init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。
https://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/ProcessingEnvironment.html
ProcessingEnvironment
- process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。
*getSupportedAnnotationTypes(): 这里返回一个set集合,告诉你需要支持的哪些注解类,要把你的注解类全路径写成字符串返回给他。
*getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返SourceVersion.latestSupported()。
java7以后可以使用,,但是看网上大家都说不建议这么使用,为了版本兼容
@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
// 合法注解全名的集合
})
这里我们需要加入两个工具框架
第一个是AutoService,因为生成注解需要一个特定的格式
AutoService可以帮我们自动生成这些包和路径,就省得自己创建了,尤其android是木有META-INF的
基友网地址
https://github.com/google/auto/tree/master/service
第二个工具是javapoet,JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。
参考链接 :https://blog.csdn.net/xuguobiao/article/details/72775730
基友网地址: https://github.com/square/javapoet
我们现在开始写编译期注解
第一步,在AnroidStudio 项目上新建两个modle,分别取名为processor,processor_lib,processor依赖processor_lib,app依赖processor和processor_lib。processor_lib存放我们自定义注解,processor用来编译。
创建
app依赖processor和processor_libl
processor依赖processor_lib
在processor引入我们的AutoService和javapoet
引入AutoService和javapoet
最后出来效果就是下图这样子
下面开始编写代码
我们在processor_lib写下我们的注解@Leo,这块不清楚的可以看我写的自定义注解篇
@Target(ElementType.FIELD)//声明在字段
@Retention(RetentionPolicy.CLASS)//声明为编译期注解
public @interface Leo {
String path();//俩参数
String name();
}
在我们的processor开启我们的注解生成部分
覆盖process,init,getSupportedSourceVersion,getSupportedAnnotationTypes 四个方法
getSupportedSourceVersion方法比较简单,直接返回支持最新的
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
getSupportedAnnotationTypes,返回支持我们的Leo注解,getCanonicalName和和getName其实一样,getName()返回的是虚拟机里面的class的表示,而getCanonicalName()返回的是更容易理解的表示。其实对于大部分class来说这两个方法没有什么不同的。但是对于array或内部类来说是有区别的。
另外,类加载(虚拟机加载)的时候需要类的名字是getName。
详情可以看这篇博客
https://blog.csdn.net/hustzw07/article/details/71108945
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> set = new HashSet<>();
set.add(Leo.class.getCanonicalName());
return set;
}
在我们的类上加上AutoService标示
AutoService
init方法
这里有几个知识点ProcessingEnvironment,Messager,Elements
private Filer filer;
private Messager messager;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
elementUtils = processingEnvironment.getElementUtils();
}
ProcessingEnvironment
这玩意还得是看API,虽然英文的但是我们可以翻译啊哈哈
大体意思就是可以提供方法进行编写新文件、报告错误消息和查找其他实用工具。具体可以自己看看
https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/ProcessingEnvironment.html
process方法
在这里处理我们注解以及生成类对象
有几个知识点
- TypeElement API
http://www.cjsdn.net/Doc/JDK60/javax/lang/model/element/TypeElement.html
通过下图我们可以得到各种类型的TypeElement
盗个图 - RoundEnvironment 直接看api
http://www.cjsdn.net/Doc/JDK60/javax/annotation/processing/RoundEnvironment.html
-
Element 我们上文说的 TypeElement,VariableElement,PackageElemtent大家都可以看看api
http://www.cjsdn.net/Doc/JDK60/javax/lang/model/element/package-summary.html -
ElementKind
http://www.cjsdn.net/Doc/JDK60/javax/lang/model/element/ElementKind.html
开始编写process,我们先整理下思路,我们在通过编译期可以拿到所有符合我们要求注解字段,生成我们想要的包,类和根据符合要求做的操作
那么可以分为一下几步
- 以类做key,拿到当前类所有的符合要求注解
- 按照类<注解>形式生成类
- 将类写到我们特定的包里
- 如果多包我们还可以按照总包-包名-类名这种形式进行分层
按照如下思路我们写下代码
//类名
String element4className;
//存放类名和元素注解的集合,HashMap保证唯一
HashMap<String, ArrayList<VariableElement>> map = new HashMap<>();
//存放元素的集合
ArrayList<VariableElement> varList;
//拿到被leo标注的所有注解元素
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Leo.class);
//遍历elements
for (Element element : elements) {
//返回此元素的类型。
ElementKind elementKind = element.getKind();
//如果类型是 作用在字段上
if (elementKind == ElementKind.FIELD) {
//那么element就是一个VariableElement
VariableElement var = (VariableElement) element;
//我们要得到它上层类 返回封装此元素(非严格意义上)的最里层元素。
TypeElement element4class = (TypeElement) var.getEnclosingElement();
//拿到返回此类型元素的完全限定名称
element4className = element4class.getQualifiedName().toString();
//利用elementUtils 然后 此包的完全限定名称
String packageName = elementUtils.getPackageOf(element4class).toString();
//判断是不是null,如果是null就生成新的存放进去
varList = map.get(element4className);
if (varList == null) {
varList = new ArrayList<VariableElement>();
map.put(element4className, varList);
}
//队列里木有就加进去
if (!varList.contains(var)) {
varList.add(var);
}
}
}
到这里还没结束,我们要根据我们发现的注解进行操作,这里书写类和包,我建议最好看下javapoet怎么使用
for (String key : map.keySet()) {
//根据key取出所有类
List<VariableElement> elementFileds = map.get(key);
//去掉类名最后的。class
String className = key.substring(key.lastIndexOf(".") + 1);
//类名后缀添加4Leo
className += "4Leo";
//创建一个public的4leo结尾的类
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC);
//生成一个返回值为String的方法 public static
MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("getElemens")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(String.class);
//遍历 注解上的name和path
for (VariableElement e : elementFileds) {
Leo annotation = e.getAnnotation(Leo.class);
String name = annotation.name();
String path = annotation.path();
//返回name和path
methodBuild.returns(String.class)
.addStatement("return $S", "name=" + name + "----path---" + path);
}
//创建方法
MethodSpec printNameMethodSpec = methodBuild.build();
//创建类
TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();
try {
//写出包名和类
JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
//添加注解
.addFileComment(" Leo Compile time annotations!")
.build();
javaFile.writeTo(filer);
} catch (IOException exception) {
exception.printStackTrace();
}
}
return true;
运行一下,我们发现这个路径下出现一个和我们包一样的4Leo的类
在我们页面测试一下
public class MainActivity extends AppCompatActivity {
@Leo(name = "leo", path = "MainActivity")
private String Text;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String elemens = MainActivity4Leo.getElemens();
Log.e(TAG, "onCreate: "+elemens );
}
}
看下log
Log
可见我们的自定义注解成功了
再写这篇文章,我学习翻阅了很多API,用了一天,终于啃下了这块硬骨头,学会了编译期注解我们能做很多事,比如写个low版本的路由框架,接下来我们试一下。
先看一下Arouter写的,我们虽然暂时只想实现一个页面跳转功能,但是也得把XX装足了
基本上就是app类初始化,页面添加@Route加path,然后
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化,跳转,我们也对着写,先把最重要的注解先写好
// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
ARouter.getInstance().build("/test/activity").navigation();
在processor_lib下写好我们的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface LRoute {
String path();
String name()default "";
}
processor下开启我们的注解处理
package xzzb.com.processor;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import xzzb.com.processor_lib.LRouter;
//添加AutoService注解
@AutoService(Processor.class)
public class RouterPorcessor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elementUtils;
private String packageName;
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//初始化装填我们注解的Set
HashSet<TypeElement> map = new HashSet<>();
//获取我们的注解
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(LRoute.class);
//遍历
for (Element element : elements) {
//拿到注解类型
ElementKind elementKind = element.getKind();
//如果作用于类上
if (elementKind == ElementKind.CLASS) {
//转成TypeElement
TypeElement element4Class = (TypeElement) element;
//添加进去
map.add(element4Class);
//获取包名
packageName = elementUtils.getPackageOf(element4Class).toString();
}
}
//创建类
TypeSpec.Builder classBuilder = TypeSpec.classBuilder("LRouterMap")
.addModifiers(Modifier.PUBLIC);
//javapoet Classname方法可以参考基友网,我们要生成一个HashMap<String,Sting>
ClassName hashMap = ClassName.get("java.util", "HashMap");
ClassName key = ClassName.get("java.lang", "String");
ClassName value = ClassName.get("java.lang", "String");
//生成一个map,将类名和注解值添加进去
MethodSpec.Builder build = MethodSpec.methodBuilder("getMaps").addModifiers(Modifier.PUBLIC);
TypeName listOfHoverboards = ParameterizedTypeName.get(hashMap, key, value);
build.addStatement("$T result = new $T<>()", listOfHoverboards, hashMap);
build.returns(listOfHoverboards);
for (TypeElement e : map) {
//遍历添加
String classname = e.getQualifiedName().toString();
LRoute annotation = e.getAnnotation(LRoute.class);
String path = annotation.path();
build.addStatement("result.put($S,$S)", path, classname);
}
//返回map,生成类和方法
build.addStatement("return result");
MethodSpec printNameMethodSpec = build.build();
TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();
try {
//写出去类
JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
.addFileComment(" Leo Compile time annotations !")
.build();
javaFile.writeTo(filer);
} catch (IOException exception) {
exception.printStackTrace();
}
return true;
}
//获取需要的工具
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
elementUtils = processingEnvironment.getElementUtils();
}
//支持版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
//要支持的注解
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> set = new HashSet<>();
set.add(LRoute.class.getName());
return set;
}
}
注解这块写完了,我们先在俩页面,写一下试试好用能生成么,
运行结果
发现我们的类名和path都被保存了下来,那么我们可以使用包名加类名的方式进行跳转了
跳转也需要一个Context,我们就一路模仿Arouter去写
public class LRouter {
private static LRouter lRouter;
/**
* @author Administrator
* @time 2018/10/18 10:56
* @describe 获取传入的Context
*/
public static Context getContext() {
return context;
}
private static Context context;
/**
* @author Administrator
* @time 2018/10/18 10:56
* @describe 私有化构造函数
*/
private LRouter() {
}
/**
* @author Administrator
* @time 2018/10/18 10:57
* @describe 初始化
*/
public static void init(Application application) {
context = application;
}
/**
* @author Administrator
* @time 2018/10/18 10:56
* @describe 单例模式
*/
public static LRouter getInstance() {
if (lRouter == null) {
lRouter = new LRouter();
}
return lRouter;
}
/**
* @author Administrator
* @time 2018/10/18 10:56
* @describe 拿到生成的路由表 返回一个Postcard对象
*/
public Postcard build(String path) {
LRouterMap lRouterMap = new LRouterMap();
HashMap<String, String> maps = lRouterMap.getMaps();
Postcard postcard = new Postcard();
String classNmae = maps.get(path);
postcard.setPath(classNmae);
return postcard;
}
}
public class Postcard {
//path
private String path;
//要跳转的包名
private String packageName;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
/**
* @author Administrator
* @time 2018/10/18 10:58
* @describe 跳转
*/
public void navigation() {
//判断路径是不是null
if (!TextUtils.isEmpty(path)) {
//截取包名
int pos = path.lastIndexOf(".");
packageName = path.substring(0, pos);
//根据包名和类名跳转
Intent intent = new Intent();
ComponentName componentName = new ComponentName(packageName, path);
intent.setComponent(componentName);
LRouter.getContext().startActivity(intent);
} else {
//路径为null直接返回
return;
}
}
}
我们去测试一下 跳转,成功跳转到了第二个页面
@LRoute(path = "Main")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//按钮跳转
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//跳转Main2页面
LRouter.getInstance().build("Main2").navigation();
}
});
}
}
第二个页面
到此为止我们完成了最low版本的页面跳转,其实我们可以仿照写更多功能。
这篇到此为止,再见
项目已经传送到基友网,地址 :https://github.com/594dudulang/LRouter,欢迎搞基
网友评论