原文

译者评论

本文虽然不是说明Java注解最详细或最严谨的文章,但重在于把概念讲清楚。

Spring框架或者说Java企业级开发框架的一个重点意义在于,尽量将业务逻辑代码与框架等基础设施分离开来,将应用配置与业务逻辑分离开来,加上分层的设计达到提升灵活性和可维护性之目的。Spring框架本身一大用处即是将业务代码依赖的组件进行依赖注入(参考控制反转这一概念)。关于这些依赖的配置既有老版本中的XML方法,也有自3.1起支持新的基于Java注解与源代码的配置

译者在对Java语言本身不是非常熟悉的情况下接触Spring框架,在老师傅的指引下虽然顺利完成工作,却对注解的概念、原理以及应用不甚理解,更妄论Spring框架的各个部分的理解。译者近期在做关于Spring框架与Java代码改造应用的工作,更倾向于基于源代码的配置(这部分代码和业务逻辑代码也是分离的,采取基于源代码而非XML配置的原因参考显示优于隐式原则),故欲借重拾Java与Spring框架应用之机,将注解这一如今已在Java应用各处广泛应用的一大主题理解透彻。

什么是注解?

用一个词来解释,注解就是元数据,元数据即关于数据的数据,所以注解即源代码的元数据,举一个例子:

@Override
public String toString() {
  return "这是当前对象的字符串表现形式";
}

上述代码中重载掉toString()方法并使用@Override注解。假使此处不使用@Override方法依旧可以正常工作,那么此处使用注解的好处是什么呢?@Override告诉编译器被注解的方法是一个被重载的方法(关于源代码的数据),假设在父类中并不存在此方法时,编译器会抛错(此方法并未覆盖父类的任何方法)。现在假设在编码时拼写错误将方法命名为toStrring()(多了一个r)同时没有使用@Override注解,代码仍然可以正常编译通过与运行,但结果却和意图完全朝着不同的方向而去。现在理解注解究竟是何种东西后,我们再来看看注解的正式定义。

在Java中,注解是一种特殊的结构,用来修饰类、方法、字段、参数、变量、构造方法或者包。根据Java规范请求175规定,注解是提供元数据的载体。

为何引入注解?

在引入注解以前(其实也包括之后与当下),广泛使用XML形式来描述元数据,然而一些特定的应用程序开发者群体以及架构设计师们认为XML维护性底下并引入了诸多坏处。他们认为相比XML与代码的松耦合(一些情况下甚至完全分离),需要一种和代码结合更紧密的形式。如果各位读者在网上搜索“XML对比注解”这一主题,会有相当数量很有意思的讨论;其中一点是XML配置意图即将配置与代码分离。各位读者也许会产生疑惑此二观点不恰巧相悖吗。其实此两种做法各有优缺点,我们尝试借用例子来理解。

假如我们设置应用程序全局范围的常量、参数时,由于这些项目与具体某项(业务逻辑)代码无关,那么XML配置文件更加合适。而如果我们要将某些方法暴露成为一个服务,那么在代码的其他地方使用这些方法时最好能明确知晓,所以此时使用注解更加合适。

除此之外,注解还有一个重点在于,它定义了如何在代码中提供元数据的标准形式,注解出现之前开发者都利用各自的形式来定义元数据,例如标记接口,注释,临时关键字等等,注解一扫混乱局面,引入标准化规范。

当前环境下,大多数应用开发框架将XML配置与注解组合起来发挥两者各自的特长。

注解如何工作,如何应用定制注解

写注解相当简单,我们可以将注解的定义与接口定义进行类比。比如我们来分析两个例子,其中之一是标准的@Override,另一个是我们定制的@Todo

@Target(ElementType.METHOD)
@Retention(RetenionPolicy.SOURCE)
public @interface Override {
}

看上去很可疑,@Override定义时没有任何处理,它是如何做到检查被注解的方法在父类中是否存在呢?笔者并非开玩笑,@Override注解的定义就是这么几行代码。重点来了,笔者再次强调:注解是代码的元数据,并不包含任何逻辑在内。如果注解不包含任何逻辑那么必然有其他消费者来使用这些注解元数据。注解本身只提供了被注解对象的相关信息。而注解的消费者才真正地读取这些信息并作出相应处理。

Java中标准的注解例如@Override的消费者是JDK或JVM自身,消费过程中将其转化为字节码。很自然地,这些注解并不受到应用开发者控制或者用作定制注解,所以我们需要自定义注解以及消费者。

接下来对写注解的要点逐一进行说明,前面的例子里我们看到用在注解定义上的注解

J2SE 5.0在java.lang.annotation包中提供了4个用于开发注解时使用的注解

  • @Documented 是否将注解假如Javadoc
  • @Retention 定义注解保留到什么时机
    • RetentionPolicy.SOURCE 编译时即拿掉,编译完成后这些注解即无效不会保留进字节码中,例如@Override@SuppressWarnings
    • RetentionPolicy.CLASS 类加载时拿掉,主要用于做字节码级别后置处理,比较惊奇的是这项是默认值
    • RetentionPolicy.RUNTIME 保留至运行时,运行时进行反射时使用,一般情况下都是使用这种自定义注解
  • @Inherited 控制注解是否会影响子类
  • @Target 可以被注解的范围,如果不指定,注解可以被用于任何目标,下面说明各项
    • ElementType.TYPE 类,接口,枚举
    • ElementType.FIELD 实例成员变量
    • ElementType.METHOD 方法
    • ElementType.PARAMETER 方法的参数
    • ElementType.CONSTRUCTOR 构造函数
    • ElementType.LOCAL_VARIABLE 本地变量
    • ElementType.ANNOTATION_TYPE 作用于其他注解
    • ElementType.PACKAGE

注解内部只能包含原始数据类型,枚举。注解的属性都定义为方法,可以提供默认值。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Todo {
  public enum Priority {LOW, MEDIUM, HIGH}
  public enum Status {STARTED, NOT_STARTED}
  String author() default "Yash";
  Priority priority() default Priority.LOW;
  Status status() default Status.NOT_STARTED;
}

下面是一个应用自定义注解的例子

@Todo(priority = Todo.Priority.MEDIUM, author = "Yashwant", status = Todo.Status.STARTED)
public void incompleteMethod1() {
  // 一部分业务逻辑开始编码
  // 不过还没全部写完
}

如果注解只有一个属性,其名字应该为value并且应用赋值时可以省略不写

@interface Author{
  String value();
}

@Author("Yashwant")
public void someMethod() {
}

以上,我们自定义出注解并将之应用于业务逻辑的方法上。现在该来看看利用反射机制编写注解的消费者;熟悉反射机制的读者应该了解反射提供了类,方法与成员对象。所有这些对象都有getAnnotation方法返回其注解对象,我们须将其转化为自定义注解类型(利用instanceof判断)

Class businessLogicClass = BusinessLogic.class;
for(Method method : businessLogicClass.getMethods()) {
  Todo todoAnnotation = (Todo)method.getAnnotation(Todo.class);
  if(todoAnnotation != null) {
    System.out.println(" Method Name : " + method.getName());
    System.out.println(" Author : " + todoAnnotation.author());
    System.out.println(" Priority : " + todoAnnotation.priority());
    System.out.println(" Status : " + todoAnnotation.status());
  }
}

注解的应用情景

注解的应用非常强大,例如Spring和Hibernate框架将其大量应用与日志与验证机制。在原本使用标记接口的地方同样可以应用注解,注解具有更大的灵活性,例如可以将其中一个方法暴露成服务。

__END__