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)的注解都可以交由注解处理器处理。现在使用注解好像有几种比较奇怪的模式:

  1. SOURCE几乎是不用的
  2. CLASS+AnnotationProcessor
  3. 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 注解及注解处理器概述的主要内容,如果未能解决你的问题,请参考以下文章

Java 注解及注解处理器概述

Java注解处理器

Spring Boot源码分析@EnableAutoConfiguration注解@AutoConfigurationImportSelector注解的处理

java注解

java注解

Android APT注解处理器 ( 根据注解生成 Java 代码 )