菜鸟笔记
提升您的技术认知

注解处理器(apt)是什么?-ag真人游戏

上一篇讲完注解,这篇咱们科普一下注解的其中一种用途——注解处理器(apt),文章会手把手的帮助大家学会apt的使用,并使用简单的例子来进行练习。

注解处理器(annotation processing tool,简称apt),是jdk提供的工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。处理方式大部分都是根据注解的信息生成新的java代码与文件。

apt使用相当广泛,eventbus、arouter、butterknife等流行框架都使用了该技术。

2.1 创建注解模块

在项目中新建module,选择【java or kotlin library】,名字和包名随意填入,点击finish。

② 在模块中定义注解,注解保留范围选择source即可(因为apt是作用在源码阶段的,生成class之前),当然选择class和runtime也可以,因为他们都包含source阶段。

我们创建一个test注解,并且包含int、string、class、string[]四种类型。

@target(elementtype.type)
@retention(retentionpolicy.source)
public @interface test {
  
    int key();
    string value() default "";
    class clazz();
    string[] array() default {
  };
}

2.2 创建注解处理器模块

① 在项目中新建module,选择【java or kotlin library】,名字和包名随意填入,点击finish。

② 修改新建module的build.gradle文件,根据是否使用kotlin分为两种情况

项目不使用kotlin:

apply plugin: "java-library"
dependencies {
  
	// 注解模块(必选)
    implementation project(':lib-annotation')
	// 注解处理器(必选)
    compileonly 'com.google.auto.service:auto-service:1.0-rc7'
    annotationprocessor 'com.google.auto.service:auto-service:1.0-rc7'
    // 生成代码方式之一(可选)
    implementation 'com.squareup:javapoet:1.13.0'
}
sourcecompatibility = javaversion.version_1_8
targetcompatibility = javaversion.version_1_8

项目使用kotlin:

apply plugin: "java-library"
apply plugin: "kotlin"
apply plugin: "kotlin-kapt"
dependencies {
  
	// 注解模块(必选)
    implementation project(':lib-annotation')
    // 注解处理器(必选)
    kapt 'com.google.auto.service:auto-service:1.0-rc7'
    implementation 'com.google.auto.service:auto-service:1.0-rc7'
    // 生成代码方式之一(可选)
    implementation 'com.squareup:javapoet:1.13.0'
}
sourcecompatibility = javaversion.version_1_8
targetcompatibility = javaversion.version_1_8

2.3 创建注解处理器

在2.2的注解处理器模块中新建类,继承自abstractprocessor类
使用@autoservice、@supportedannotationtypes、@supportedsourceversion注解注释该类,注解处理器即创建完成,具体如下:

// 让该类拥有了获取注解的能力(必选)
@autoservice(processor.class)
// 设置该处理器支持哪几种注解(必选)
// 字符串类型,例:com.kproduce.annotation.test
@supportedannotationtypes({
  const.card_annotation,const.test_annotation})
// 源码版本(可选)
@supportedsourceversion(sourceversion.release_8)
public class testannotationprocessor extends abstractprocessor {
  
    @override
    public synchronized void init(processingenvironment processingenv) {
  
        super.init(processingenv);
    }
    
    @override
    public boolean process(set annotations, roundenvironment roundenv) {
  
        return false;
    }
}

2.4 在app模块中引入注解处理器

在主项目app模块的build.gradle中引入注解处理器,使用kapt或annotationprocessor修饰,所以在gradle中看到这两种修饰的项目就是注解处理器项目了。

dependencies {
  
	// 注解模块
    implementation project(":lib-annotation")
    // 注解处理器模块,以下二选一
    // 使用kotlin选择这种
    kapt project(":compiler")
    // 使用java选择这种
    annotationprocessor project(":compiler")
}

2.5 测试

经过上面的一系列操作,注解处理器已经注册完成,在其process方法中会接收到想要处理的注解。

① 在项目中使用@test注解

@test(id = 100, desc = "person类", clazz = person.class, array = {
  "111", "aaa", "bbb"})
public class person {
  
}

② 在注解处理器的init方法中,可以通过processingenvironment参数获取messager对象(可以打印日志),在process方法中获取到注解后输出日志查看被注解的类名。(注意:如果打印日志使用diagnostic.kind.error,会中断构建)

@autoservice(processor.class)
@supportedannotationtypes(const.test_annotation)
@supportedsourceversion(sourceversion.release_8)
public class testannotationprocessor extends abstractprocessor {
  
    private messager messager;
    @override
    public synchronized void init(processingenvironment processingenv) {
  
        super.init(processingenv);
        // 获取messager对象
        messager = processingenv.getmessager();
    }
    @override
    public boolean process(set annotations, roundenvironment roundenv) {
  
    	// 获取所有的被test注解的对象,无论是类还是属性都会被封装成element
        for (element element : roundenv.getelementsannotatedwith(test.class)) {
  
            messager.printmessage(diagnostic.kind.note, ">>>>>>>>>>>>>>>getannotation:"   element.getsimplename());
        }
        return false;
    }
}

③ 构建项目,查看日志,成功获取到注解注释过的类名

获取到了注解,我们看一下如何正确的拿到注解内的信息,在注解处理器中类、方法、属性都会被形容成element,由于我们定义的@test只修饰类,所以element也都是类。

@autoservice(processor.class)
@supportedannotationtypes(const.test_annotation)
@supportedsourceversion(sourceversion.release_8)
public class testannotationprocessor extends abstractprocessor {
  
    private messager messager;
    // 这个是处理element的工具
    private elements elementtool;
    @override
    public synchronized void init(processingenvironment processingenv) {
  
        super.init(processingenv);
        messager = processingenv.getmessager();
        elementtool = processingenv.getelementutils();
    }
    @override
    public boolean process(set annotations, roundenvironment roundenv) {
  
    	// 拿到被test修饰的element,因为我们只修饰类,所以拿到的element都是类
        for (element element : roundenv.getelementsannotatedwith(test.class)) {
  
        	// ===============获取当前被修饰的类的信息===============
        	
        	// 获取包名,例:com.kproduce.androidstudy.test
            string packagename = elementtool.getpackageof(element).getqualifiedname().tostring();
            // 获取类名,例:person
            string classname = element.getsimplename().tostring();
			// 拼装成文件名,例:com.kproduce.androidstudy.test.person
            string filename = packagename   const.dot   classname;
            
			// ===============解析注解===============
			
			// 获取注解
            test card = element.getannotation(test.class);
			// 注解中的int值
            int id = card.id();
            // 注解中的string
            string desc = card.desc();
            // 注解中的数组[]
            string[] array = card.array();
            // 获取类有比较奇葩的坑,需要特别注意!
			// 在注解中拿class然后调用getname()会抛出mirroredtypeexception异常
			// 处理方式可以通过捕获异常后,在异常中获取类名
            string dataclassname;
            try {
  
                dataclassname = card.clazz().getname();
            } catch (mirroredtypeexception e) {
  
                dataclassname = e.gettypemirror().tostring();
            }
        }
        return true;
    }
}

获取到了注解信息,下一步就是根据注解生成java代码了。但是生成代码的意义是什么呢?

答:wmrouter路由在获取到了注解信息之后,会根据注解的内容生成路由注册的代码。 把一些复杂的可能写错的冗长的代码变成了自动生成,避免了人力的浪费和错误的产生。下面是wmrouter生成的代码:

目前生成java代码有两种方式,原始方式和javapoet。原始方式理解即可,咱们使用javapoet来解析注解、生成代码。

4.1 原始方式

原始方式就是通过流一行一行的手写代码。
优点:可读性高。
缺点:复用性差。

咱们看一下eventbus的源码就能更深刻的理解什么是原始方式:

// 截取eventbusannotationprocessor.java中的片段
private void createinfoindexfile(string index) {
  
    bufferedwriter writer = null;
    try {
  
        javafileobject sourcefile = processingenv.getfiler().createsourcefile(index);
        int period = index.lastindexof('.');
        string mypackage = period > 0 ? index.substring(0, period) : null;
        string clazz = index.substring(period   1);
        writer = new bufferedwriter(sourcefile.openwriter());
        if (mypackage != null) {
  
            writer.write("package "   mypackage   ";\n\n");
        }
        writer.write("import org.greenrobot.eventbus.meta.simplesubscriberinfo;\n");
        writer.write("import org.greenrobot.eventbus.meta.subscribermethodinfo;\n");
        writer.write("import org.greenrobot.eventbus.meta.subscriberinfo;\n");
        writer.write("import org.greenrobot.eventbus.meta.subscriberinfoindex;\n\n");
        writer.write("import org.greenrobot.eventbus.threadmode;\n\n");
        writer.write("import java.util.hashmap;\n");
        writer.write("import java.util.map;\n\n");
        writer.write("/** this class is generated by eventbus, do not edit. */\n");
        writer.write("public class "   clazz   " implements subscriberinfoindex {\n");
        writer.write("    private static final map, subscriberinfo> subscriber_index;\n\n");
        writer.write("    static {\n");
        writer.write("        subscriber_index = new hashmap, subscriberinfo>();\n\n");
        writeindexlines(writer, mypackage);
        writer.write("    }\n\n");
        writer.write("    private static void putindex(subscriberinfo info) {\n");
        writer.write("        subscriber_index.put(info.getsubscriberclass(), info);\n");
        writer.write("    }\n\n");
        writer.write("    @override\n");
        writer.write("    public subscriberinfo getsubscriberinfo(class subscriberclass) {\n");
        writer.write("        subscriberinfo info = subscriber_index.get(subscriberclass);\n");
        writer.write("        if (info != null) {\n");
        writer.write("            return info;\n");
        writer.write("        } else {\n");
        writer.write("            return null;\n");
        writer.write("        }\n");
        writer.write("    }\n");
        writer.write("}\n");
    } catch (ioexception e) {
  
        throw new runtimeexception("could not write source for "   index, e);
    } finally {
  
        if (writer != null) {
  
            try {
  
                writer.close();
            } catch (ioexception e) {
  
                //silent
            }
        }
    }
}

4.2 javapoet

javapoet是使用java的api和面向对象思想来生成.java文件的库。
优点:面向对象思想、复用性高。
缺点:学习成本高、可读性一般。

因为学习点比较多,咱们仅对用到的api进行说明,其他的可以参考github地址,里面有相当全面的教程。

4.2.1 生成代码

我们先用javapoet生成一个helloworld类,下面是我们想要的java代码:

package com.example.helloworld;
public final class helloworld {
  
  public static void main(string[] args) {
  
    system.out.println("hello, javapoet!");
  }
}

在javapoet中使用了面向对象的思想,万物皆对象,方法和类也变成了对象。在类中代码主要被分为了两块,一块是方法(methodspec),一块是类(typespec)。

接下来我们使用javapoet生成这段代码,比较易懂。
① 先创建main方法的methodspec对象;
② 再创建helloworld类的typespec对象,将main方法传入。

// 创建main方法的methodspec对象
methodspec main = methodspec.methodbuilder("main")	// 方法名:main
    .addmodifiers(modifier.public, modifier.static)	// 方法修饰:public static
    .returns(void.class)	// 返回类型 void
    .addparameter(string[].class, "args")	// 参数:string[] args
    .addstatement("$t.out.println($s)", system.class, "hello, javapoet!")	// 内容system.out.println("hello, javapoet!");
    .build();
    
// 创建helloworld类的typespec对象,将main方法传入
typespec helloworld = typespec.classbuilder("helloworld")	// 类名:helloworld
    .addmodifiers(modifier.public, modifier.final)	// 类修饰:public final
    .addmethod(main)	// 添加方法main
    .build();
// 构建生成文件,第一个参数为包名
javafile javafile = javafile.builder("com.example.helloworld", helloworld)
    .build();
javafile.writeto(system.out);

经过以上步骤就可以生成helloworld类。

4.2.2 javapoet中的自定义类型

上面代码中会发现几个奇怪的类型写在字符串中,详细的讲解可以查看github地址。

$l:值,可以放各种对象,比如int,object等。
$s:字符串。
$t:类的引用,会自动导入该类的包,比如new date()中的date。
$n:定义好的method方法名,可以调用代码中的其他方法。

4.2.3 各种案例

提供几个案例,更好的理解javapoet,详细的讲解可以查看github地址。

① 循环

void main() {
  
  int total = 0;
  for (int i = 0; i < 10; i  ) {
  
    total  = i;
  }
}
// javapoet方式 1
methodspec main = methodspec.methodbuilder("main")
    .addstatement("int total = 0")
    .begincontrolflow("for (int i = 0; i < 10; i  )")
    .addstatement("total  = i")
    .endcontrolflow()
    .build();
    
// javapoet方式 2
methodspec main = methodspec.methodbuilder("main")
    .addcode(""
          "int total = 0;\n"
          "for (int i = 0; i < 10; i  ) {\n"
          "  total  = i;\n"
          "}\n")
    .build();

② arraylist

package com.example.helloworld;
import com.mattel.hoverboard;
import java.util.arraylist;
import java.util.list;
public final class helloworld {
  
  list beyond() {
  
    list result = new arraylist<>();
    result.add(new hoverboard());
    result.add(new hoverboard());
    result.add(new hoverboard());
    return result;
  }
}
// javapoet方式
classname hoverboard = classname.get("com.mattel", "hoverboard");
classname list = classname.get("java.util", "list");
classname arraylist = classname.get("java.util", "arraylist");
typename listofhoverboards = parameterizedtypename.get(list, hoverboard);
methodspec beyond = methodspec.methodbuilder("beyond")
    .returns(listofhoverboards)
    .addstatement("$t result = new $t<>()", listofhoverboards, arraylist)
    .addstatement("result.add(new $t())", hoverboard)
    .addstatement("result.add(new $t())", hoverboard)
    .addstatement("result.add(new $t())", hoverboard)
    .addstatement("return result")
    .build();

③ 属性

public class helloworld {
  
  private final string android;
  private final string robot;
}
// javapoet方式
fieldspec android = fieldspec.builder(string.class, "android")
    .addmodifiers(modifier.private, modifier.final)
    .build();
    
typespec helloworld = typespec.classbuilder("helloworld")
    .addmodifiers(modifier.public)
    .addfield(android)
    .addfield(string.class, "robot", modifier.private, modifier.final)
    .build();

最后咱们再总结一下apt。

  1. apt是jdk提供的工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。
  2. 获取到注解后可以使用原始方法与javapoet生成java代码。

这样apt的介绍就结束了,希望大家读完这篇文章,会对apt有一个更深入的了解。如果我的文章能给大家带来一点点的福利,那在下就足够开心了。

网站地图