java注解框架

阅读目录

我们经常会在 java 代码里面看到:“@Override”,“@Target”等等样子的东西,这些是什么?

在 java 里面它们是“注解”。

下面是百度百科的解释:java.lang.annotation.Retention 可以在您定义 Annotation 型态时,指示编译器如何对待您的自定义 Annotation,

预设上编译器会将Annotation资讯留在 class 档案中,但不被虚拟机器读取,而仅用于编译器或工具程式运行时提供资讯。

也就是说,注解是建立在 class 文件基础上的东西,同 C 语言的宏有异曲同工的效果。

class 文件里面根本看不到注解的痕迹。

注解的基础就是反射。所以注解可以理解为 java 特有的一种概念。

回到顶部

1. 元注解

在 java.lang.annotation 包里面,已经定义了 4 种 annotation 的“原语”。

1).@Target, 用于明确被修饰的类型:(方法,字段,类,接口等等)  
2).@Retention, 描述 anntation 存在的为止:

RetentionPolicy.RUNTIME 注解会在 class 字节码文件中存在,在运行时可以通过反射获取到

RetentionPolicy.CLASS 默认的保留策略,注解会在 class 字节码文件中存在,但运行时无法获得

RetentionPolicy.SOURCE 注解仅存在于源码中,在 class 字节码文件中不包含

3).@Documented, 默认情况下,注解不会在 javadoc 中记录,但是可以通过这个注解来表明这个注解需要被记录。
4).@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。

如果一个使用了 @Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该 class 的子类。

回到顶部

2. 自定义注解

package com.joyfulmath.jvmexample.annnotaion;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • @author deman.lu
  • @version on 2016-05-23 13:36
    */

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FruitName {

String value() </span><span style="color: rgba(0, 0, 255, 1)">default</span> ""<span style="color: rgba(0, 0, 0, 1)">;

}

首先,一个注解一般需要 2 个元注解修饰:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)

具体作用上面已解释。

所有的注解都会有一个类似于“func”的部分。这个可以理解为注解的参数。

package com.joyfulmath.jvmexample.annnotaion;

import com.joyfulmath.jvmexample.TraceLog;

/**

  • @author deman.lu

  • @version on 2016-05-23 13:37
    */
    public class Apple {

    @FruitName("Apple")
    String appleName;

    public void displayAppleName()
    {
    TraceLog.i(appleName);
    }
    }

这段代码的 log:

05-23 13:39:38.780 26792-26792/com.joyfulmath.jvmexample I/Apple: displayAppleName: null [at (Apple.java:16)]

没有赋值成功,为什么?应为注解的“Apple”到底怎么赋值该 filed,目前编译器还不知道则怎么做呢。

回到顶部

3. 注解处理器

我们还需要一个处理器来解释 注解到底是怎样工作的,不然就跟注释差不多了。

通过反射的方式,可以获取注解的内容:

package com.joyfulmath.jvmexample.annnotaion;

import com.joyfulmath.jvmexample.TraceLog;

import java.lang.reflect.Field;

/**

  • @author deman.lu
  • @version on 2016-05-23 14:08
    */
    public class FruitInfoUtils {
    public static void getFruitInfo(Class<?> clazz)
    {
    String fruitNameStr
    = "";
    Field[] fields
    = clazz.getDeclaredFields();
    for(Field field:fields)
    {
    if(field.isAnnotationPresent(FruitName.class))
    {
    FruitName fruitName
    = field.getAnnotation(FruitName.class);
    fruitNameStr
    = fruitName.value();
    TraceLog.i(fruitNameStr);
    }
    }
    }
    }

 

这是注解的一般用法。

 

回到顶部

android 注解框架解析

从上面可以看到,注解框架的使用,本质上还是要用到反射。

但是我如果用反射的功能在使用注解框架,那么,我还不如直接使用它,反而简单。

如果有一种机制,可以避免写大量重复的相似代码,尤其在 android 开发的时候,大量的 findviewbyid & onClick 等事件相应。

代码的模式是一致的,但是代码又各不相同,这个时候,使用注解框架可以大量节省开发时间,当然相应的会增加其他的开销。

以下就是一个使用 butterknife 的例子:

@BindString(R.string.login_error) 
String loginErrorMessage;

看上去很简单,就是把字符串赋一个 string res 对应的初值。这样写可以节省一些时间。当然这只是一个例子,

如果大量使用其他的注解,可以节省很大一部分的开发时间。

我们下面来看看怎么实现的:

package butterknife;

import android.support.annotation.StringRes;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.CLASS;

/**

  • Bind a field to the specified string resource ID.
  • <pre><code>
  • {@literal @}BindString(R.string.username_error) String usernameErrorText;
  • </code></pre>
    /
    @Retention(CLASS) @Target(FIELD)
    public @interface BindString {
    /** String resource ID to which the field will be bound.
    /
    @StringRes
    int value();
    }

BindString, 只有一个参数,value, 也就是赋值为 @StringRes.

同上,上面是注解定义和使用的地方,但是真正解释注解的地方如下:ButterKnifeProcessor

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env)

这个函数,截取部分代码:

    // Process each @BindString element.
    for (Element element : env.getElementsAnnotatedWith(BindString.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {parseResourceString(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindString.class, e);}
    }

找到所有 BindString 注解的元素,然后开始分析:

private void parseResourceString(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<TypeElement> erasedTargetNames) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Verify that the target type is String.</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">STRING_TYPE.equals(element.asType().toString())) {
  error(element, </span>"@%s field type must be 'String'. (%s.%s)"<span style="color: rgba(0, 0, 0, 1)">,
      BindString.</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getSimpleName(), enclosingElement.getQualifiedName(),
      element.getSimpleName());
  hasError </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Verify common generated code restrictions.</span>
hasError |= isInaccessibleViaGeneratedCode(BindString.<span style="color: rgba(0, 0, 255, 1)">class</span>, "fields"<span style="color: rgba(0, 0, 0, 1)">, element);
hasError </span>|= isBindingInWrongPackage(BindString.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">, element);

</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (hasError) {
  </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Assemble information on the field.</span>
String name =<span style="color: rgba(0, 0, 0, 1)"> element.getSimpleName().toString();
</span><span style="color: rgba(0, 0, 255, 1)">int</span> id = element.getAnnotation(BindString.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">).value();

BindingClass bindingClass </span>=<span style="color: rgba(0, 0, 0, 1)"> getOrCreateTargetClass(targetClassMap, enclosingElement);
FieldResourceBinding binding </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> FieldResourceBinding(id, name, "getString", <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">);
bindingClass.addResource(binding);

erasedTargetNames.add(enclosingElement);

}

首先验证 element 是不是 string 类型。

 // Assemble information on the field.
    String name = element.getSimpleName().toString();
    int id = element.getAnnotation(BindString.class).value();

获取 field 的 name,以及 string id。

最终

Map<TypeElement, BindingClass> targetClassMap 

元素和注解描述,已 map 的方式一一对应存放。

  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
</span><span style="color: rgba(0, 0, 255, 1)">for</span> (Map.Entry&lt;TypeElement, BindingClass&gt;<span style="color: rgba(0, 0, 0, 1)"> entry : targetClassMap.entrySet()) {
  TypeElement typeElement </span>=<span style="color: rgba(0, 0, 0, 1)"> entry.getKey();
  BindingClass bindingClass </span>=<span style="color: rgba(0, 0, 0, 1)"> entry.getValue();

  </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
    <span style="color: rgba(255, 0, 0, 1)">bindingClass.brewJava().writeTo(filer);</span>
  } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException e) {
    error(typeElement, </span>"Unable to write view binder for type %s: %s"<span style="color: rgba(0, 0, 0, 1)">, typeElement,
        e.getMessage());
  }
}

</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;

}

这就是注解框架启动的地方,一个独立的进程。具体细节本文不研究,只需清除,这里是框架驱动的地方。

从上面的信息已经清除,所有的注解信息都存放在 targetClassMap 里面。

上面标红的代码,应该是注解框架的核心之处。

自从 Java SE5 开始,Java 就引入了 apt 工具,可以对注解进行预处理,Java SE6,更是支持扩展注解处理器,

并在编译时多趟处理,我们可以使用自定义注解处理器,在 Java 编译时,根据规则,生成新的 Java 代码。

JavaFile brewJava() {
    TypeSpec.Builder result = TypeSpec.classBuilder(generatedClassName)
        .addModifiers(PUBLIC);
    if (isFinal) {result.addModifiers(Modifier.FINAL);
    } else {
      result.addTypeVariable(TypeVariableName.get("T", targetTypeName));}
TypeName targetType </span>= isFinal ? targetTypeName : TypeVariableName.get("T"<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (hasParentBinding()) {
  result.superclass(ParameterizedTypeName.get(parentBinding.generatedClassName, targetType));
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
  result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, targetType));
}

result.addMethod(createBindMethod(targetType));

</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (isGeneratingUnbinder()) {
  result.addType(createUnbinderClass(targetType));
} </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">isFinal) {
  result.addMethod(createBindToTargetMethod());
}

</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> JavaFile.builder(generatedClassName.packageName(), result.build())
    .addFileComment(</span>"Generated code from Butter Knife. Do not modify!"<span style="color: rgba(0, 0, 0, 1)">)
    .build();

}

这段话的关键是会 create 一个新文件。

然后把相关内容写入。

 

参考:

https://github.com/JakeWharton/butterknife