一、基本概念
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。
我们都知道在Java代码中使用注释是为了提升代码的可读性,也就是说,注释是给人看的(对于编译器来说没有意义)。注解可以看做是注释的“强力升级版",它可以向编译器、虚拟机等解释说明一些事情(也就是说它对编译器等工具也是“可读”的)。比如我们非常熟悉的@Override注解,它的作用是告诉编译器它所注解的方法是重写的父类中的方法,这样编译器就会去检查父类是否存在这个方法,以及这个方法的签名与父类是否相同。
也就是说,注解是描述Java代码的代码,它能够被编译器解析,注解处理工具在运行时也能够解析注解。除了向编译器等传递一些信息,我们也可以使用注解生成代码。比如我们可以使用注解来描述我们的意图,然后让注解解析工具来解析注解,以此来生成一些”模板化“的代码。比如Hibernate、Spring等框架大量使用了注解,来避免一些重复的工作。注解是一种”被动“的信息,必须由编译器或虚拟机来“主动”解析它,它才能发挥自己的作用。
使用Annotation之前(甚至在使用之后),XML被广泛的应用于描述元数据。不知何时开始一些应用开发人员和架构师发现XML的维护越来越糟糕了。他们希望使用一些和代码紧耦合的东西,而不是像XML那样和代码是松耦合的(在某些情况下甚至是完全分离的)代码描述。如果你在Google中搜索“XML vs. annotations”,会看到许多关于这个问题的辩论。最有趣的是XML配置其实就是为了分离代码和配置而引入的。上述两种观点可能会让你很疑惑,两者观点似乎构成了一种循环,但各有利弊。下面我们通过一个例子来理解这两者的区别。
假如你想为应用设置很多的常量或参数,这种情况下,XML是一个很好的选择,因为它不会同特定的代码相连。如果你想把某个方法声明为服务,那么使用Annotation会更好一些,因为这种情况下需要注解和方法紧密耦合起来,开发人员也必须认识到这点。
另一个很重要的因素是Annotation定义了一种标准的描述元数据的方式。在这之前,开发人员通常使用他们自己的方式定义元数据。例如,使用标记interfaces,注释,transient关键字等等。每个程序员按照自己的方式定义元数据,而不像Annotation这种标准的方式。
目前,许多框架将XML和Annotation两种方式结合使用,平衡两者之间的利弊。
二、元注解
元注解即用来描述注解的注解。Java5.0定义的元注解:
- @Target,注解用于什么地方
- @Retention,什么时候使用该注解
- @Documented,注解是否将包含在JavaDoc中
- @Inherited,是否允许子类继承该注解
1.@Target
用于描述注解的使用范围(即:被描述的注解可以用在什么地方)。取值(ElementType)有:
1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述实例变量
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于记录java文件的package信息
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
2.@Retention
表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)。取值(RetentionPoicy)有:
- SOURCE:
在源文件中有效(即源文件保留),在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。 - CLASS:
在class文件中有效(即class保留),在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。 - RUNTIME:
在运行时有效(即运行时保留),始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
3.@Documented
****@****Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
**4.@Inherited **
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
三、自定义注解
参考
扯扯Java反射与注解
怎样理解 java 注解和运用注解编程
怎样优雅地使用java注解
retrofit 注解学习
要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation对象,因为你除此之外没有别的获取注解对象的方法。
<pre>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Example{
public static String STRING_DATA = "String";
public String name() default "New Example";
public String description();
public int id() default 0;
}
</pre>
自定义注解方式与JAVA接口的定义非常相似。不同的是,interface关键字前面加上了 “@”来作出区分这是注解而不是接口。并且注解定义中你可以看到在 name()和id()后面都有一个" default“,这表示当你使用该注解时,如果你没有指定这项的值,它将会使用default后面的作为默认值。而注解上方的另外两个注解则指定了注解的有效策略和注解对应的对象类型。
<pre>
//IConfig.java:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IConfig {
public String name();
public String type();
public static String DInteger = "Integer";
public static String DDouble = "Double";
public static String DString = "String";
public static String DLong = "Long";
public static String DChar = "Character";
}
//status.java:
public class Status {
@IConfig(name = "ontime.starttime", type = IConfig.DInteger)
public static int data = 1;
public static final int d = 2;
}
//test:
public static void main(String[] args) throws ClassNotFoundException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, SecurityException, InvocationTargetException{
//加载这个类
Class<?> cls = Class.forName("cn.sunflyer.test.Status");
//这里是获取类中的全部公开的变量 即变量修饰符需要为public
Field fs[] = cls.getFields();
if(fs != null){
//遍历变量数组
for(Field x:fs){
//打印出变量名称和修饰符的值,可以通过Modifier类中的常量对比获取修饰符类型
System.out.println("名称 " + x.getName() + " 修饰符 " + x.getModifiers());
Annotation rms = x.getAnnotation(IConfig.class);
//如果获取到IConfig注解,则输出内容并根据注解动态修改数据
if(rms != null){
IConfig ic = (IConfig)rms;
System.out.println("注解 name : " + ic.name());
System.out.println("注解 type : " + ic.type());
String da = "1234";
Object ras = null;
if(ic.type().equals(IConfig.DString)){
ras = da;
}else{
//加载JAVA基本数据类型的封装对象类
Class<?> fCls = Class.forName("java.lang." + ic.type());
//获取指定方法
Method ms = fCls.getMethod("valueOf", String.class);
//调用方法
ras = ms.invoke(null , da);
}
x.set(null, ras);
}
}
}
}
</pre>
运行后的结果为:
名称 data 修饰符 9
注解 name : ontime.starttime
注解 type : Integer
名称 d 修饰符 25
四、android注解 ButterKnife
ButterKnife框架原理
深入理解 ButterKnife,让你的程序学会写代码
Android注解那些事儿
专门为Android View设计的绑定注解ButterKnife
偷懒插件Android Butterknife Zelezny
使用 @Bind注解并传入一个View ID,Butter Knife 就可以找到并且自动地对你的布局中的View进行转换并绑定到类成员上。
class ExampleActivity extends Activity {
@Bind(R.id.title) TextView title;
@Bind(R.id.subtitle) TextView subtitle;
@Bind(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
请注意,相比与缓慢的反射机制,Butter Knife的代码是生成的,因此不必担心注解的性能问题。可能很多人都觉得ButterKnife在bind(this)方法执行的时候通过反射获取ExampleActivity中所有的带有@Bind注解的属性并且获得注解中的R.id.xxx值,最后还是通过反射拿到Activity.findViewById()方法获取View,并赋值给ExampleActivity中的某个属性.这是一个注解库的实现方式,比较原始,一个很大的缺点就是在Activity运行时大量使用反射会影响App的运行性能,造成卡顿以及生成很多临时Java对象更容易触发GC。ButterKnife显然没有使用这种方式,它用了Java Annotation Processing技术,就是在Java代码编译成Java字节码的时候就已经处理了。
为什么你用@Bind、@OnClick等注解标注的属性或方法必须是public或protected的,因为ButterKnife是通过ExampleActivity.this.editText
来注入View的。
为什么要这样呢?有些注入框架比如roboguice你是可以把View设置成private的,答案就是性能。如果你把View设置成private,那么框架必须通过反射来注入View,不管现在手机的CPU处理器变得多快,如果有些操作会影响性能,那么是肯定要避免的,这就是ButterKnife与其他注入框架的不同。
调用bind来生成这些代码,你可以查看或调试这些代码。
例如上面的例子,生成的代码大致如下所示:
public void bind(ExampleActivity activity) {
activity.subtitle = (android.widget.TextView) activity.findViewById(2130968578);
activity.footer = (android.widget.TextView) activity.findViewById(2130968579);
activity.title = (android.widget.TextView) activity.findViewById(2130968577);
}
绑定资源到类成员上可以使用@BindBool、@BindColor、@BindDimen、@BindDrawable、@BindInt、@BindString。使用时对应的注解需要传入对应的id资源,例如@BindString你需要传入R.string.id_string的字符串的资源id。
class ExampleActivity extends Activity {
@BindString(R.string.title) String title;
@BindDrawable(R.drawable.graphic) Drawable graphic;
@BindColor(R.color.red) int red; // int or ColorStateList field
@BindDimen(R.dimen.spacer) Float spacer; // int (for pixel size) or float (for exact value) field
// ...
}
侦听器绑定
@OnClick(R.id.submit)
public void submit() {
// TODO submit data to server...
}
@OnClick(R.id.submit)
public void sayHi(Button button) {
button.setText("Hello!");
}
//同时指定多个id的控件到同一个事件监听上:
@OnClick({ R.id.door1, R.id.door2, R.id.door3 })
public void pickDoor(DoorView door) {
if (door.hasPrizeBehind()) {
Toast.makeText(this, "You win!", LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Try again", LENGTH_SHORT).show();
}
}