Java 注解及注解处理器概述
Posted 一口仨馍
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 注解及注解处理器概述相关的知识,希望对你有一定的参考价值。
元注解
- @Documented 注解是否将包含在JavaDoc中
- @Inherited 被注解类的子类是否会注解
- @Retention 注解保留位置
- @Target 注解的范围
@Retention注解value属性的取值范围为枚举类RetentionPolicy的三个枚举值,SOURCE、CLASS和RUNTIME。分别对应注解的保留位置为.java源码、.class代码和字节码。
SOURCE类型的注解只保留在.java代码中,一般作为提示开发规范。例如@Override、@SuppressWarnings等。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override
CLASS类型的注解保留到.class文件中,一般配合processor处理,下文会重点讲这个,暂时略过。
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE)
public @interface NotNull
RUNTIME类型的注解会保留到字节码中,一般当做字节码中的标记,反射调用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable
Class<? extends Annotation> value();
@Target可取值如下
- ElementType.TYPE:用于描述类、接口或enum声明
- ElementType.FIELD:用于描述实例变量
- ElementType.METHOD
- ElementType.PARAMETER
- ElementType.CONSTRUCTOR
- ElementType.LOCAL_VARIABLE
- ElementType.ANNOTATION_TYPE 另一个注释
- ElementType.PACKAGE 用于记录java文件的package信息
Java8新增
Java8中注解可以写在任意地方,相对灵活一些,新增的两个注释的程序元素类型 ElementType.TYPE_USE和 ElementType.TYPE_PARAMETER。新增@Repeatable注解,Java8之前如果一个属性可以有多个值,会定义一个数组,Java8之后可以对同一属性多次使用统一注解,增加了点可读性。讲真,这些变化,我觉得意义不大。感兴趣的还请自行查看。
SOURCE和CLASS类型的注解
见下文注解处理器
RUNTIME类型的注解
RUNTIME类型的注解比较简单,一般使用反射获取。一般使用套路是:首先编写需要的注解,之后在被注解的类中增加一行“初始化/注册”代码,代码内部通过拿到被注解的属性,然后反射相应的方法或者自行处理一些逻辑。比较典型的有Android xUtils3源码解析之注解模块、Android EventBus3.0使用及源码解析。由于大量反射会消耗些性能,有些框架比如ButterKnife、ARouter用的是CLASS+AnnotationProcessor类型的注解。但是随着设备性能的提升,反射消耗的性能体现越来越不明显。RUNTIME类型的注解套路比较简单,对比着看xUtils3和EventBus3的代码,相信读者能很快看懂。
注解处理器(Annotation Processor)
注解处理器是Javac的一个工具,在编译时扫描和处理注解,在AbstractProcessor#process回调中可以做想做的事,通常用来生成.java辅助代码。自定义三种类型(SOURCE、CLASS、RUNTIME)的注解都可以交由注解处理器处理。现在使用注解好像有几种比较奇怪的模式:
- SOURCE几乎是不用的
- CLASS+AnnotationProcessor
- RUNTIME+反射
实际上并没有这种模式的限制,SOURCE、CLASS、RUNTIME都可以由注解处理器处理,由于保留位置的限制,反射一般和RUNTIME搭配。笔者尝试将ARouter中的Route注解(CLASS+AnnotationProcessor模式)修改为SOURCE类型,工程照样运行。综上,个人觉得SOURCE+AnnotationProcessor才是最优的注解模式。
注解处理器关的注册可以由'com.google.auto.service:auto-service:1.0-rc2'
实现,aotu-service其实也是个注解,功能是提供@AutoService注解和AutoServiceProcessor辅助生成注解处理器注册文件。当然,你也可以手动在main 目录下手动新建 resources/META-INF/services文件夹,然后在上述文件夹内新建javax.annotation.processing.Processor文件,最后在上述文件中写入注解处理器全路径。
关于辅助生成.java代码目前我只知道ARouter和ButterKnife都用的api 'com.squareup:javapoet:1.7.0'
。javapoet框架提供了很多API去辅助生成.java文件,属性、方法、类名等等。还一种比较原始的方式是自己创建文件,然后手动写入一个个字符串。
注解的主要作用是标记与携带属性,以上可以拿到标记也辅助生成了.java代码,但是这些.java代码是没有实例化的,相当于只是编写了.java文件却没有任何调用。ARouter的做法是在所有dex文件中获取所有类,然后按照指定规则提取出需要的类,最后反射获取实例、调用实例方法。大致代码如下
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException
final Set<String> classNames = new HashSet<>();
// 所有dex文件路径
List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
for (final String path : paths)
DefaultPoolExecutor.getInstance().execute(new Runnable()
@Override
public void run()
DexFile dexfile = null;
try
if (path.endsWith(EXTRACTED_SUFFIX))
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
else
dexfile = new DexFile(path);
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements())
String className = dexEntries.nextElement();
if (className.startsWith(packageName))
classNames.add(className);
catch (Throwable ignore)
Log.e("ARouter", "Scan map file in dex files made error.", ignore);
finally
...
parserCtl.countDown();
);
parserCtl.await();
Log.d(Consts.TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
for (String className : routerMap)
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT))
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance()))
.loadInto(Warehouse.groupsIndex);
ButterKnife的做法如下
ButterKnife.bind(this);
public static Unbinder bind(@NonNull Activity target)
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
private static Unbinder createBinding(@NonNull Object target, @NonNull View source)
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null)
return Unbinder.EMPTY;
try
return constructor.newInstance(target, source);
catch (IllegalAccessException e)
...
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls)
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
try
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
catch (ClassNotFoundException e)
...
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
查找由AnnotationProcessor依据注解生成的类,并通过反射调用生成类的实例,在生成类的构造方法中调用findViewById、SetOnClickListener等。
写在最后
注解这东西存在的意义就是API的使用。编写难点并不在于注解本身。如果是准备用RUNTIME+反射的话,需要点反射的知识。如果是使用Processor的话,需要去了解下javapoet的一些语法(相信读者不愿意以最原始的方式写字符串)。无论哪种方式,困难度都不大,只是需要一些额外小知识点。本文概述注解及注解处理器的使用流程,如果想较深入了解这方面,建议看下EventBus3、ButterKnife、xUtils3、ARouter这种带注解的框架,最好能自己写个小demo练习下,毕竟学以致用才是最终目的~
以上是关于Java 注解及注解处理器概述的主要内容,如果未能解决你的问题,请参考以下文章
Spring Boot源码分析@EnableAutoConfiguration注解@AutoConfigurationImportSelector注解的处理