Android编译时注解框架(五)-语法讲解

Android编译时注解框架-语法讲解

原文链接

概述

本章内容主要对APT一些语法进行简单讲解。apt的学习资料真的太少了,我的学习方法基本上只能通过看开源库的源码猜、看源码注释猜、自己运行着猜……

这里对猜对的结果进行一个总结,让后来者可以更快的上手。

第一次写这种类型的博客,总结的可能有些分散,建议结合开源库源码学习。

自定义注解相关

定义注解格式: public @interface 注解名 {定义体}

Annotation里面的参数该设定:

第一,只能用public或默认(default)这两个访问权修饰.例如,String value();不能是private;

第二,参数只能使用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数类型就为String;

@Retention(RetentionPolicy.CLASS)
	@Target(ElementType.TYPE)
	public @interface GetMsg {
	    int id();  //注解参数
	    String name() default "default";
	}
	
	//使用
	@GetMsg(id = 1,name = "asd")
	class Test{
	}

如果只有一个参数,建议设置为value

@Retention(RetentionPolicy.CLASS)
	@Target(ElementType.TYPE)
	public @interface Println {
	    int value();
	}
	
	//使用
	@Println(1)
	class Test{
	}

参数为value时,可以直接写入参数,使用时不在需要key=value写法。
但当有多个参数时,不可以再使用value。

@Retention

这个在第一章有讲。申明该注解属于什么类型注解

  • @Retention(RetentionPolicy.SOURCE)

    源码时注解,一般用来作为编译器标记。就比如Override, Deprecated, SuppressWarnings这样的注解。(这个我们一般都很少自定义的)

  • @Retention(RetentionPolicy.RUNTIME)

    运行时注解,一般在运行时通过反射去识别的注解。

  • @Retention(RetentionPolicy.CLASS)

    编译时注解,在编译时处理。

@Target(ElementType.TYPE)

表示该注解用来修饰哪些元素。并可以修饰多个

@Retention(RetentionPolicy.CLASS)
	@Target({ElementType.LOCAL_VARIABLE,ElementType.METHOD})
	public @interface GetMsg {
	    int id();
	    String name() default "default";
	}

例如 GetMsg只能用在局部变量和方法上,如果修饰到类上编译器会报错。

@GetMsg(1)
    public void printError(){
        //TODO ~
    }
    
    @GetMsg(1)  //编译器会报错
    class Test{
     	//TODO ~
    }
  • @Target(ElementType.TYPE)

    接口、类、枚举、注解

  • @Target(ElementType.FIELD)

    字段、枚举的常量

  • @Target(ElementType.METHOD)

    方法

  • @Target(ElementType.PARAMETER)

    方法参数

  • @Target(ElementType.CONSTRUCTOR)

    构造函数

  • @Target(ElementType.LOCAL_VARIABLE)

    局部变量

  • @Target(ElementType.ANNOTATION_TYPE)

    注解

  • @Target(ElementType.package)

@Inherited

该注解的字面意识是继承,但你要知道注解是不可以继承的

@Inherited是在继承结构中使用的注解。

如果你的注解是这样定义的:

	@Inherited
	@Retention(RetentionPolicy.CLASS)
	@Target(ElementType.TYPE)
	public @interface Test {
		//...
	}

当你的注解定义到类A上,此时,有个B类继承A,且没使用该注解。但是扫描的时候,会把A类设置的注解,扫描到B类上。

这里感谢 豪哥 @刘志豪 的排疑解惑~

注解的默认值

注解可以设置默认值,有默认值的参数可以不写。

@Retention(RetentionPolicy.CLASS)
	@Target(ElementType.TYPE)
	public @interface GetMsg {
	    int id();  //注解参数
	    String name() default "default";
	}
	
	//使用
	@GetMsg(id = 1) //name有默认值可以不写
	class Test{
	}
“注解的继承”(依赖倒置?)

这里讲的继承并不是通过@Inherited修饰的注解。

这个“继承”是一个注解的使用技巧,使用上的感觉类似于依赖倒置,来自于ButterKnife源码。

先看代码。

@Target(METHOD)
	@Retention(CLASS)
	@ListenerClass(
	    targetType = "android.view.View",
    	setter = "setOnClickListener",
    	type = "butterknife.internal.DebouncingOnClickListener",
    	method = @ListenerMethod(
        	name = "doClick",
        	parameters = "android.view.View"
    	)
	)
	public @interface OnClick {
  		/** View IDs to which the method will be bound. */
  		int[] value() default { View.NO_ID };
	}

这是ButterKnife的OnClick 注解。特殊的地方在于**@OnClick修饰了注解@ListenerClass**,并且设置了一些只属于@OnClick的属性。

那这样的作用是什么呢?

凡是修饰了@OnClick的地方,也就自动修饰了@ListenerClass。类似于@OnClick是@ListenerClass的子类。而ButterKnife有很多的监听注解@OnItemClick、@OnLongClick等等。

这样在做代码生成时,不需要再单独考虑每一个监听注解,只需要处理@ListenerClass就OK。

处理器类Processor编写

自定义注解后,需要编写Processor类处理注解。Processor继承自AbstractProcessor的类。

AbstractProcessor有两个重要的方法需要重写。

在这里插入图片描述

重写getSupportedAnnotationTypes方法:

通过重写该方法,告知Processor哪些注解需要处理。

返回一个Set集合,集合内容为自定义注解的包名+类名。

建议项目中这样编写:

	@Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        //需要全类名
        types.add(GetMsg.class.getCanonicalName()); 
        types.add(Println.class.getCanonicalName());
        return types;
    }

另外如果注解数量很少的话,可以通过另一种方式实现:

//在只有一到两个注解需要处理时,可以这样编写:
@SupportedAnnotationTypes("com.example.annotation.SetContentView")
	@SupportedSourceVersion(SourceVersion.RELEASE_7)
	public class ContentViewProcessor extends AbstractProcessor {
	
	}
重写process方法:

所有的注解处理都是从这个方法开始的,你可以理解为,当APT找到所有需要处理的注解后,会回调这个方法,你可以通过这个方法的参数,拿到你所需要的信息。

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        
        
        return false;
    }

先简单解释下这个方法的参数和返回值。

参数 Set<? extends TypeElement> annotations :将返回所有的由该Processor处理,并待处理的 Annotations。(属于该Processor处理的注解,但并未被使用,不存在与这个集合里)
参数 RoundEnvironment roundEnv :表示当前或是之前的运行环境,可以通过该对象查找找到的注解。

例:

	for (Element element : env.getElementsAnnotatedWith(GetMsg.class)) {
		//所有被使用的@GetMsg
	}

返回值 表示这组 annotations 是否被这个 Processor 接受,如果接受(true)后续子的 pocessor 不会再对这个 Annotations 进行处理

输出Log

虽然是编译时执行Processor,但也是可以输入日志信息用于调试的。

Processor日志输出的位置在编译器下方的Messages窗口中。

Processor支持最基础的System.out方法。

同样Processor也有自己的Log输出工具: Messager。

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    	
    	//取得Messager对象
        Messager messager = processingEnv.getMessager();
        
        //输出日志
        messager.printMessage(Diagnostic.Kind.NOTE,
                    "Annotation class : className = " + element.getSimpleName().toString());
        }

同Log类似,Messager也有日志级别的选择。

  • Diagnostic.Kind.ERROR
  • Diagnostic.Kind.WARNING
  • Diagnostic.Kind.MANDATORY_WARNING
  • Diagnostic.Kind.NOTE
  • Diagnostic.Kind.OTHER

他们的输出样式如图:

在这里插入图片描述

注意:当没有属于该Process处理的注解被使用时,process不会执行。

注意:如果发现替换jar后,apt代码并没有执行,尝试clean项目。

这里你会发现输出了两次日志信息。其原因在于APT扫描了源码两次,可为什么要扫描两次?

用生成的代码来生成代码

APT可以扫描源码中的所有注解,依据这些注解来生成代码,那么生成的代码中如果也有注解呢?

同样可以被扫描到,并且用于代码生成。其过程如下:

APT第一次扫描源码中的所有注解,扫描结束后生成代码,之后再扫描一次,以保证生成的代码中的注解也可以被扫描到,第二次扫描到注解后继续生成代码,类似于递归一样的【扫描 - 代码生成 - 扫描 - 代码生成 - 扫描 - 代码生成 - 扫描 - 代码生成】。一直到扫描到的注解为0时停止。

同样你肯定也会发现一个问题,这不很容易会变成死循环吗?

没错,所以在生成的代码中一定要慎重出现编译时注解,把控好你的代码逻辑!

Element

Element也是APT的重点之一,所有通过注解取得元素都将以Element类型等待处理,也可以理解为Element的子类类型与自定义注解时用到的@Target是有对应关系的。

Element的官方注释:

Represents a program element such as a package, class, or method.
Each element represents a static, language-level construct (and not, for example, a runtime construct of the virtual machine).

表示一个程序元素,比如包、类或者方法。

例如:取得所有修饰了@OnceClick的元素。

	for (Element element : roundEnv.getElementsAnnotatedWith(OnceClick.class)){
		//OnceClick.class是@Target(METHOD)
		//则该element是可以强转为表示方法的ExecutableElement
		ExecutableElement method =ExecutableElement)element;
		//如果需要用到其他类型的Element,则不可以直接强转,需要通过下面方法转换
		//但有例外情况,我们稍后列举
		TypeElement classElement = (TypeElement) element
                    .getEnclosingElement();
	}

Element的子类有:

  • ExecutableElement

    表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。

    对应@Target(ElementType.METHOD) @Target(ElementType.CONSTRUCTOR)

  • PackageElement;

    表示一个包程序元素。提供对有关包极其成员的信息访问。

    对应@Target(ElementType.PACKAGE)

  • TypeElement;

    表示一个类或接口程序元素。提供对有关类型极其成员的信息访问。

    对应@Target(ElementType.TYPE)

    注意:枚举类型是一种类,而注解类型是一种接口。

  • TypeParameterElement;

    表示一般类、接口、方法或构造方法元素的类型参数。

    对应@Target(ElementType.PARAMETER)

  • VariableElement;

    表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。

    对应@Target(ElementType.LOCAL_VARIABLE)

例如:@OnceClick的@Target(METHOD)。其修饰方法,那么在这个情况下:

Element 可以直接强制转换为ExecutableElement。而其他类型的Element不能直接强制转,需要其他办法。

	for (Element element : roundEnv.getElementsAnnotatedWith(OnceClick.class)){
		ExecutableElement method = (ExecutableElement)element;
	}

接下来我们将以@Target()分类进行讲解,不同Element的信息获取方式不同。

修饰方法的注解和ExecutableElement

当你有一个注解是以@Target(ElementType.METHOD)定义时,表示该注解只能修饰方法。

那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、方法名、参数类型、返回值。

如何获取:

//OnceClick.class 以 @Target(ElementType.METHOD)修饰
	for (Element element : roundEnv.getElementsAnnotatedWith(OnceClick.class)) {
		//对于Element直接强转
        ExecutableElement executableElement = (ExecutableElement) element;
        
        //非对应的Element,通过getEnclosingElement转换获取
        TypeElement classElement = (TypeElement) element
                    .getEnclosingElement();
                    
        //当(ExecutableElement) element成立时,使用(PackageElement) element
        //            .getEnclosingElement();将报错。
        //需要使用elementUtils来获取
        Elements elementUtils = processingEnv.getElementUtils();
        PackageElement packageElement = elementUtils.getPackageOf(classElement);
		
		//全类名
        String fullClassName = classElement.getQualifiedName().toString();
        //类名
        String className = classElement.getSimpleName().toString();
        //包名
        String packageName = packageElement.getQualifiedName().toString();
        //方法名
        String methodName = executableElement.getSimpleName().toString();

		//取得方法参数列表
		List<? extends VariableElement> methodParameters = executableElement.getParameters();
		//参数类型列表
		List<String> types = new ArrayList<>();
        for (VariableElement variableElement : methodParameters) {
            TypeMirror methodParameterType = variableElement.asType();
            if (methodParameterType instanceof TypeVariable) {
                TypeVariable typeVariable = (TypeVariable) methodParameterType;
                methodParameterType = typeVariable.getUpperBound();
                
            }
            //参数名
            String parameterName = variableElement.getSimpleName().toString();
            //参数类型
            String parameteKind = methodParameterType.toString();
            types.add(methodParameterType.toString());
        }
	}

修饰属性、类成员的注解和VariableElement

当你有一个注解是以@Target(ElementType.FIELD)定义时,表示该注解只能修饰属性、类成员。

那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、类成员类型、类成员名

如何获取:

for (Element element : roundEnv.getElementsAnnotatedWith(IdProperty.class)) {
		//ElementType.FIELD注解可以直接强转VariableElement
		VariableElement variableElement = (VariableElement) element;
		
        TypeElement classElement = (TypeElement) element
                .getEnclosingElement();
        PackageElement packageElement = elementUtils.getPackageOf(classElement);
        //类名
        String className = classElement.getSimpleName().toString();
        //包名
        String packageName = packageElement.getQualifiedName().toString();
        //类成员名
        String variableName = variableElement.getSimpleName().toString();
        
        //类成员类型
        TypeMirror typeMirror = variableElement.asType();
        String type = typeMirror.toString();
        
	}

修饰类的注解和TypeElement

当你有一个注解是以@Target(ElementType.TYPE)定义时,表示该注解只能修饰类、接口、枚举。

那么这个时候你为了生成代码,而需要获取一些基本信息:包名、类名、全类名、父类。

如何获取:

for (Element element : roundEnv.getElementsAnnotatedWith(xxx.class)) {
		//ElementType.TYPE注解可以直接强转TypeElement
        TypeElement classElement = (TypeElement) element;
        
        PackageElement packageElement = (PackageElement) element
                    .getEnclosingElement();
                    
        //全类名
        String fullClassName = classElement.getQualifiedName().toString();
        //类名
        String className = classElement.getSimpleName().toString();
        //包名
        String packageName = packageElement.getQualifiedName().toString();
 		//父类名
 		String superClassName = classElement.getSuperclass().toString();
        
	}
	


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值