注册

Android修炼系列(五),写一篇超全面的annotation讲解(2)

自定义编译期注解(CLASS)


为什么要最后说编译期注解呢,因为相对前面的自定义注解来说,编译期注解有些难度,涉及到的东西比较多,但却是平时用到的最多的注解,因为编译期注解不存在反射,所以对性能没有影响。


本来也想用绑定 View 的例子讲解,但是现在这样的 demo 网上各种泛滥,而且还有各路大牛写的,所以我就没必要班门弄斧了。在这里以跳转界面为例:


    Intent intent = new Intent (this, NextActivity.class);
startActivity (intent);
复制代码

本着方便就是改进的原则,让我们定义一个编译期注解,来自动生成上述的代码,想想每次需要的时候只需要一个注解就能跳转到想要跳转的界面是不是很刺激。


1.首先新建一个 android 项目,在创建两个 java module(File -> New -> new Module ->java Module),因为有的类在android项目中不支持,建完后项目结构如下:


这里写图片描述


其中 annotation 中盛放自定义的注解,annotationprocessor 中创建注解处理器并做相关处理,最后的 app 则为我们的项目。


注意:MyFirstProcessor类为上文讲解 AbstractProcessor 所建的类,可以删去,跟本项目没有关系。


2.处理各自的依赖


annotation


processor


app


3.编写自定义注解,这是一个应用到字段之上的注解,被注解的字段为传递的参数


/**
* 这是一个自定义的跳转传值所用到的注解。
* value 表示要跳转到哪个界面activity的元素,传入那个界面的名字。
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface IntentField {
String value () default " ";
}
复制代码

4.自定义注解处理器,获取被注解元素的类型,进行相应的操作。


@AutoService(javax.annotation.processing.Processor.class)
public class MyProcessot extends AbstractProcessor{

private Map<Element, List<VariableElement>> items = new HashMap<>();
private List<Generator> generators = new LinkedList<>();

// 做一些初始化工作
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
Utils.init();
generators.add(new ActivityEnterGenerator());
generators.add(new ActivityInitFieldGenerator());
}

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

// 获取所有注册IntentField注解的元素
for (Element elem : roundEnvironment.getElementsAnnotatedWith(IntentField.class)) {
// 主要获取ElementType 是不是null,即class,interface,enum或者注解类型
if (elem.getEnclosingElement() == null) {
// 直接结束处理器
return true;
}

// 如果items的key不存在,则添加一个key
if (items.get(elem.getEnclosingElement()) == null) {
items.put(elem.getEnclosingElement(), new LinkedList<VariableElement>());
}

// 我们这里的IntentField是应用在一般成员变量上的注解
if (elem.getKind() == ElementKind.FIELD) {
items.get(elem.getEnclosingElement()).add((VariableElement)elem);
}
}

List<VariableElement> variableElements;
for (Map.Entry<Element, List<VariableElement>> entry : items.entrySet()) {
variableElements = entry.getValue();
if (variableElements == null || variableElements.isEmpty()) {
return true;
}
// 去通过自动javapoet生成代码
for (Generator generator : generators) {
generator.genetate(entry.getKey(), variableElements, processingEnv);
generator.genetate(entry.getKey(), variableElements, processingEnv);
}
}
return false;
}

// 指定当前注解器使用的Java版本
@Override public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

// 指出注解处理器 处理哪种注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>(2);
annotations.add(IntentField.class.getCanonicalName());
return annotations;
}
}
复制代码

5.这是一个工具类方法,提供了本 demo 中所用到的一些方法,其实实际里面的方法都很常见,只不过做了一个封装而已.


public class Utils {

private static Set<String> supportTypes = new HashSet<>();

/** 当getIntent的时候,每种类型写的方式都不一样,所以把每种方式都添加到了Set容器中。*/
static void init() {
supportTypes.add(int.class.getSimpleName());
supportTypes.add(int[].class.getSimpleName());
supportTypes.add(short.class.getSimpleName());
supportTypes.add(short[].class.getSimpleName());
supportTypes.add(String.class.getSimpleName());
supportTypes.add(String[].class.getSimpleName());
supportTypes.add(boolean.class.getSimpleName());
supportTypes.add(boolean[].class.getSimpleName());
supportTypes.add(long.class.getSimpleName());
supportTypes.add(long[].class.getSimpleName());
supportTypes.add(char.class.getSimpleName());
supportTypes.add(char[].class.getSimpleName());
supportTypes.add(byte.class.getSimpleName());
supportTypes.add(byte[].class.getSimpleName());
supportTypes.add("Bundle");
}

/** 获取元素所在的包名。*/
public static String getPackageName(Element element) {
String clazzSimpleName = element.getSimpleName().toString();
String clazzName = element.toString();
return clazzName.substring(0, clazzName.length() - clazzSimpleName.length() - 1);
}


/** 判断是否是String类型或者数组或者bundle,因为这三种类型getIntent()不需要默认值。*/
public static boolean isElementNoDefaultValue(String typeName) {
return (String.class.getName().equals(typeName) || typeName.contains("[]") || typeName.contains("Bundle"));
}

/**
* 获得注解要传递参数的类型。
* @param typeName 注解获取到的参数类型
*/
public static String getIntentTypeName(String typeName) {
for (String name : supportTypes) {
if (name.equals(getSimpleName(typeName))) {
return name.replaceFirst(String.valueOf(name.charAt(0)), String.valueOf(name.charAt(0)).toUpperCase())
.replace("[]", "Array");
}
}
return "";
}

/**
* 获取类的的名字的字符串。
* @param typeName 可以是包名字符串,也可以是类名字符串
*/
static String getSimpleName(String typeName) {
if (typeName.contains(".")) {
return typeName.substring(typeName.lastIndexOf(".") + 1, typeName.length());
}else {
return typeName;
}
}


/** 自动生成代码。*/
public static void writeToFile(String className, String packageName, MethodSpec methodSpec, ProcessingEnvironment processingEnv, ArrayList<FieldSpec> listField) {
TypeSpec genedClass;
if(listField == null) {
genedClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(methodSpec).build();
}else{
genedClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(methodSpec)
.addFields(listField).build();
}
JavaFile javaFile = JavaFile.builder(packageName, genedClass)
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}

}

复制代码

6.自定义一个接口,把需要自动生成的每个java文件的方法都独立出去。


public interface Generator {
void genetate(Element typeElement
, List<VariableElement> variableElements
, ProcessingEnvironment processingEnv);

}
复制代码

7.编写自动生成文件的格式,生成后的类格式如下:


跳转类格式


上图为本例中的MainActivity$Enter类,如果你想生成一个类,那么这个类的格式和作用肯定已经在你的脑海中有了定型,如果你自己都不知道想要生成啥,那还玩啥。


/**
* 这是一个要自动生成跳转功能的.java文件类
* 主要思路:1.使用javapoet生成一个空方法
* 2.为方法加上实参
* 3.方法的里面的代码拼接
* 主要需要:获取字段的类型和名字,获取将要跳转的类的名字
*/
public class ActivityEnterGenerator implements Generator{

private static final String SUFFIX = "$Enter";

private static final String METHOD_NAME = "intentTo";

@Override
public void genetate(Element typeElement, List<VariableElement> variableElements, ProcessingEnvironment processingEnv) {
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.returns(void.class);
// 设置生成的METHOD_NAME方法第一个参数
methodBuilder.addParameter(Object.class, "context");
methodBuilder.addStatement("android.content.Intent intent = new android.content.Intent()");

// 获取将要跳转的类的名字
String name = "";

// VariableElement 主要代表一般字段元素,是Element的一种
for (VariableElement element : variableElements) {
// Element 只是一种语言元素,本身并不包含信息,所以我们这里获取TypeMirror
TypeMirror typeMirror = element.asType();
// 获取注解在身上的字段的类型
TypeName type = TypeName.get(typeMirror);
// 获取注解在身上字段的名字
String fileName = element.getSimpleName().toString();
// 设置生成的METHOD_NAME方法第二个参数
methodBuilder.addParameter(type, fileName);
methodBuilder.addStatement("intent.putExtra(\"" + fileName + "\"," +fileName + ")");
// 获取注解上的元素
IntentField toClassName = element.getAnnotation(IntentField.class);
String name1 = toClassName.value();
if(null != name && "".equals(name)){
name = name1;
}
// 理论上每个界面上的注解value一样,都是要跳转到的那个类名字,否则提示错误
else if(name1 != null && !name1.equals(name)){
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "同一个界面不能跳转到多个活动,即value必须一致");
}
}
methodBuilder.addStatement("intent.setClass((android.content.Context)context, " + name +".class)");
methodBuilder.addStatement("((android.content.Context)context).startActivity(intent)");

/**
* 自动生成.java文件
* 第一个参数:要生成的类的名字
* 第二个参数:生成类所在的包的名字
* 第三个参数:javapoet 中提供的与自动生成代码的相关的类
* 第四个参数:能够为注解器提供Elements,Types和Filer
*/
Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX, Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv,null);
}

}
复制代码

当我们定义了跳转的类,那么接下来肯定就是在另一个界面获取传递过来的数据了,参考格式如下,这是本demo中自动生成的MainActivity$Init 类。


获取参数格式


/**
* 要生成一个.Java文件,在这个Java文件里生成一个获取上个界面传递过来数据的方法
* 主要思路:1.使用Javapoet生成一个空的的方法
* 2.为方法添加需要的形参
* 3.拼接方法内部的代码
* 主要需要:获取传递过来字段的类型
*/
public class ActivityInitFieldGenerator implements Generator {

private static final String SUFFIX = "$Init";

private static final String METHOD_NAME = "initFields";

@Override
public void genetate(Element typeElement, List<VariableElement> variableElements, ProcessingEnvironment processingEnv) {

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PROTECTED)
.returns(Object.class);

final ArrayList<FieldSpec> listField = new ArrayList<>();

if (null != variableElements && variableElements.size() != 0) {
VariableElement element = variableElements.get(0);
// 当前接收数据的字段的名字
IntentField currentClassName = element.getAnnotation(IntentField.class);
String name = currentClassName.value();

methodBuilder.addParameter(Object.class, "currentActivity");
methodBuilder.addStatement(name + " activity = (" + name + ")currentActivity");
methodBuilder.addStatement("android.content.Intent intent = activity.getIntent()");
}

for (VariableElement element : variableElements) {

// 获取接收字段的类型
TypeName currentTypeName = TypeName.get(element.asType());
String currentTypeNameStr = currentTypeName.toString();
String intentTypeName = Utils.getIntentTypeName(currentTypeNameStr);

// 字段的名字,即key值
Name filedName = element.getSimpleName();

// 创建成员变量
FieldSpec fieldSpec = FieldSpec.builder(TypeName.get(element.asType()),filedName+"")
.addModifiers(Modifier.PUBLIC)
.build();
listField.add(fieldSpec);

// 因为String类型的获取 和 其他基本类型的获取在是否需要默认值问题上不一样,所以需要判断是哪种
if (Utils.isElementNoDefaultValue(currentTypeNameStr)) {
methodBuilder.addStatement("this."+filedName+"= intent.get" + intentTypeName + "Extra(\"" + filedName + "\")");
} else {
String defaultValue = "default" + element.getSimpleName();
if (intentTypeName == null) {
// 当字段类型为null时,需要打印错误信息
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "the type:" + element.asType().toString() + " is not support");
} else {
if ("".equals(intentTypeName)) {
methodBuilder.addStatement("this." + filedName + "= (" + TypeName.get(element.asType()) + ")intent.getSerializableExtra(\"" + filedName + "\")");
} else {
methodBuilder.addParameter(TypeName.get(element.asType()), defaultValue);
methodBuilder.addStatement("this."+ filedName +"= intent.get"
+ intentTypeName + "Extra(\"" + filedName + "\", " + defaultValue + ")");
}
}
}
}
methodBuilder.addStatement("return this");
Utils.writeToFile(typeElement.getSimpleName().toString() + SUFFIX, Utils.getPackageName(typeElement), methodBuilder.build(), processingEnv, listField);
}
}
复制代码

8、在Activity中使用刚才的自定义注解。


public class MainActivity extends AppCompatActivity {

@IntentField("NextActivity")
int count = 10;
@IntentField("NextActivity")
String str = "编译器注解";
@IntentField("NextActivity")
StuBean bean = new StuBean(1,"No1");

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
addOnclickListener();
}

public void addOnclickListener() {
findViewById(R.id.tvnext).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 从哪个界面进行跳转,则以哪个界面打头,enter 结尾
// 例如 MainActivity$Enter
new MainActivity$Enter()
.intentTo(MainActivity.this, count, str, bean);
}
});
}
}
复制代码

9.这是实体bean


public class StuBean implements Serializable{
public StuBean(int id , String name) {
this.id = id;
this.name = name;
}
//学号
public int id;
//姓名
public String name;
}
复制代码

10、在NextActivity接收并打印数据:


public class NextActivity extends AppCompatActivity {

private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);
textView = (TextView) findViewById(R.id.tv);

// 想获取从哪个界面传递过来的数据,就已哪个类打头,init结尾
// 例如 MainActivity$Init
MainActivity$Init formIntent = (MainActivity$Init)new MainActivity$Init().initFields(this,0);
textView.setText(formIntent.count + "---" + formIntent.str + "---" +formIntent.bean.name);

// 打印上个界面传递过来的数据
Log.i("Tag",formIntent.count + "---" + formIntent.str + "---" + formIntent.bean.name);
}
}
复制代码

11.运行结果:


这里写图片描述


总结


好了,看到这里,你应该对注解有所了解了,但是看的再懂也不如自己动手练一下。如果你仔细研究了,你会发现一个非常奇怪的事情,当我们设置 RetentionPolicy.CLASS 级别的时候,仍能通过反射获取注解信息,当我们设置 RetentionPolicy.SOURCE 级别的时候,仍能走通编译期注解,是不是非常迷惑。


之后只能又找了一些资料(非权威),看到了一个比较受认同的解释:这个属性主要给IDE 或者编译器开发者准备的,一般应用级别上不太会用到。



好了,本文到这里就结束了,关于注解的讲解应该非常全面了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



参考 1、B.E,Java编程思想:机械工业出版社


作者:矛盾的阿呆i
链接:https://juejin.cn/post/6936609673416015908
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册