so true

心怀未来,开创未来!
随笔 - 160, 文章 - 0, 评论 - 40, 引用 - 0
数据加载中……

[转载]Java批注的发明起因及代码应用实例

 
        批注能够消除样板代码,让源代码的可读性更高,并能提供级别更高的错误检查。从EJB3到JUnit4,哪里都在使用它。本文就将告诉你如何使用它。
  Java 5向Java引入了批注(Annotations),它的使用迅速成为现代Java开发中不可缺少的一部分。在正式开始介绍它之前,看看为什么要发明批注,这是非常值得的。
  自从Java诞生之日起,人们就一直在解决它初期忽视了的一些问题:缺少元数据;缺乏将Java以外的代码嵌入到Java源代码文件里的能力等。当 Java面市的时候,针对这些问题而推出的JavaDoc终于让它变完整了。JavaDoc使用了在代码里专门标记注释的概念,从而让它能够提取出额外的信息,说具体点就是文档,并将它转换成为我们熟悉的JavaDoc文档。这是一项简单的技术,人人都可以使用。首先会有Doclet,目的是让人们扩展文档的输出。然后是Xdoclet,它像使用标记一样使用JavaDoc来生成代码,从而将整个过程变得轻而易举。这部分是对J2EE的复杂性的回应。 J2EE原来依靠很多样板代码(boilerplate code)把对象捆绑到J2EE框架里。但是这些方案都有一些问题。首先,注释里的标记从来都不会进入最终的源代码,所以除非你生成代码来反映这些标记,否则你无法在运行期间查找到它。其次,它会把整个预处理层加到(在理想情况下应该是)一个简单编译过程里。最后,基于注释的标记在编译期间并不是很容易检查,也无法轻易被很多IDE检查;如果你把注释标记拼写错了,编译器是不会注意到的,编译器只会关注那些它知道确切名字的标记。
  要解决这所有的问题,Java新增了批注。批注是用于Java语言的本机元数据标记。它们的输入严格与Java语言的其他部分类似,可以通过反映被发现,更容易地让 IDE和编译器的编写者管理。现在就让我们看一些被批注的代码吧;我们先从BaseExample开始,它是一个简单类,只带有一个方法—— myMethod:

  public class BaseExample {

  public BaseExample() {}

  public void myMethod() {

  System.out.println("This is the BaseExample");

  }

  }

  现在,我们想要扩展BaseExample并替代myMethod。下面就是完成这一任务的Example1代码:

  public class Example1 extends BaseExample {

  public Example1() {}

  @Overridepublic void myMethod() {

  System.out.println("This is the Example1");

  }

  }

  这样我们就有了第一个关于myMethod的批注——@Override。这是一系列内置的批注之一。@Override的意思是“方法必须替代其超类中的一个方法;如果做不到这一点,那么就会有东西出错,使得编译器产生错误”。没有@Override,代码照样会正常工作,但是假设有人修改BaseExample,让myMethod带有参数。如果你没有使用@Override批注,代码仍然会被编译,隐藏了子类没有替代超类方法的问题。如果有@Override的话,你会在编译期间看到发生错误。
  你可能会认为“难道语言的扩展没有解决这个问题,额外的关键字可能会吗”,是的,它可能已经实现了这一点,但是这不仅没有给语言带来任何灵活性,还会导致很多源代码兼容性的问题。批注这种方式避免了改变Java语言本身(当然除了增加了@markup),并且还能够放在代码的不同部分里,而不仅仅是在标记方法里。
  关于批注还有一点是,你可以创建自己的批注标记,这正是我们马上要讨论的内容。想一想下面这个问题:我们有一些简单的Java Beans程序,它们都带有不同的字符串字段。我们希望能够有一些通用窗体显示代码,它们能够用其他显示提示(比如宽度)来正确地标示这些字段。现在我们可以编写一个超类,它能够提取出这个数据,比如说从一个在每个类里都带有一些静态支持方法的静态数组里,但是这也意味着要强制给代码分层。利用批注做到这一点就要简单得多了。现在让我们从定义FormLabel.java里的FormLabel的批注开始:

  import java.lang.annotation.*;

  @Retention(RetentionPolicy.RUNTIME)

  @Target(ElementType.METHOD)

  public@interface FormLabel {String label();

  int width() default 40;

  }

  你应该注意到的第一件事是Java使用了它自己内置的一些批注来定批注:@Retention和@Target。@Retention用来定义通过设置 RetentionPolicy的值批注能够在构建-运行过程中存留多久。这里我们使用了RUNTIME,这意味着我们定义的批注将会在运行期间被保留在代码里。RetentionPolicy.SOURCE将被用于一个我们希望被编译器使用然后抛弃的批注。RetentionPolicy.CLASS让它们保留在生成的类文件里,但是能够在运行期间被Java虚拟机(JVM)访问到。
  在默认情况下,你可以在代码里的任何地方都应用批注。 @Target批注让你能够将它限制在代码的特定部分里。在本文里,我们把目标瞄准了ElementType.METHOD,这意味着它只能够与方法关联在一起。其他ElementTypes有CONSTRUCTOR、FIELD、LOCAL_VARIABLE、PACKAGE、PARAMETER和 TYPE,每个都能够把批注限制到该种类型的Java语言元素,所以例如,设置TYPE将只允许批注为定义过的这种类型,比如:

  @OurAnnotation

  public class OurAnnotatedClass {…

  值得注意的是,@Target批注能够接受单个ElementType或者一个ElementType数组,如果你想要将批注限制为一系列语言元素的话。
  下面一部分是批注接口的定义;这就像是一个普通的接口声明,除了我们用@interface将其标记为一个批注。在这个接口里,我们然后定义批注的方法,就像我们希望用在与批注相关联的信息上的抽象方法,所以我们就有了String label(),用于一个叫做label的字符串属性。如果我们没有方法,那么批注就只能用于“做标记”,而@Overrides注释就是这样一个例子。如果你只有一个属性,它最好被命名为“value”,因为当带有一个未命名参数的批注在设置这个值时,它工作得最好。属性还可以有默认值,比如“int width() default 40;”就是在定义一个默认值为40的整数属性。
  这就是批注定义。我们现在就可以在代码里使用它了。下面一个SimpleData类就用到了它。

  public class SimpleData {

  private String firstname;

  private String lastname;

  private String postcode;

  public SimpleData() {}

  @FormLabel(label="First Name")

  public String getFirstname() { return firstname; }

  public void

  setFirstname(String firstname) {this.firstname = firstname;}

  @FormLabel(label="Last Name",width=80)

  public String getLastname() { return lastname; }

  public void setLastname(String lastname) {

  this.lastname = lastname;

  }

  @FormLabel(label="Postal code",width=10)

  public String getPostcode() { return postcode; }

  public void setPostcode(String postcode) {

  this.postcode = postcode;

  }

  }

  当然,如果我们不查找批注,那么它们对代码的执行就不会造成任何不同。我们所需要的是在运行期间使用批注的方式;我们通过Reflection API来达到这一目的。现在就让我们创建一个简单的processForm方法,它能够在任何对象里查找批注。

  public void processForm(Object o) {

  for(Method m:o.getClass().getMethods()) {

  我们将在传递给方法的对象的类里定义所有的方法。现在,我们需要检查每个方法,看看它们是否有FormLabel批注,以及是否返回一个String(为了简单地说明问题,我们给所有的结果多返回一些代码):

  if(m.isAnnotationPresent(FormLabel.class) &&

  m.getReturnType()==String.class) {

  现在我们可以通过使用Method的getAnnotation()方法来提取FormLabel批注:

  FormLabel formLabel=

  m.getAnnotation(FormLabel.class);

  现在我们执行方法来取得其字符串值,并通过在批注接口里定义的方法访问批注属性。下面我们就把它们打印出来:

  try {

  String value=(String)m.invoke(o);

  String label=formLabel.label();

  int width=formLabel.width();

  System.out.printf("%s[%d]:%s\n",label,width,value);

  } catch (IllegalArgumentException ex) {

  ex.printStackTrace();

  }

  catch (IllegalAccessException ex) {

  ex.printStackTrace();}

  catch (InvocationTargetException ex) {

  ex.printStackTrace();

  }

  }

  }

  }

  现在我们可以创建含有@FormLabel批注的新类,并把它们传递给processForm方法。这是在运行期间访问你自己的批注的基础。
  现在这个时候,我们回头看看Java 5里面其他关于批注的内容。首先是编译器指令——@Deprecated和@SuppressWarnings。@Deprecated是把方法标示为被否定的增强方法;不推荐把它用在新代码里,以防止以后删除。用@Deprecated可以生成一个来自编译器的相关警告。

  @SuppressWarnings会阻止编译器在封闭代码元素里警告你,所以你可以在类定义的开始或者对特定的方法使用@SuppressWarnings。它可以带参数,用来指定需要取消的错误的类型,例如:

  @SuppressWarnings("unchecked")

  public List getList() {

  List l=new LinkedList();

  return l;

  }

  这里我们取消了一个关于在List和List之间的“未检查”的强制转换。当你开始用Java编程但是没有非一般代码的时候,这就非常有用。在取消警告的时候,尽可能地缩小取消的范围是值得的;在上面的例子里,我们取消了整个代码。我们可以把它变紧凑,只隐藏一个语句的错误:

  public List

  getListToo() {

  @SuppressWarnings("unchecked")

  List l=new LinkedList();

  return l;

  }

  要注意的是,你需要在Java2SE 1.5.06或者以上的版本上进行这项工作;这之前的版本没有提供对@SuppressWarning支持。
  Java 5里其他内置的批注都与对批注的支持有关——@Documented和@Inherited。它们都可以被加到批注定义里。@Documented的作用是,批注的使用应该在所有生成的JavaDoc文档里都反映出来。正如你可能看到的,批注和JavaDoc标记是互补的。@Inherited的意思是,当另外一个类用类来扩展批注时,批注应该是可继承的;在默认情况下,批注是不能被继承的。

  你可能很希望在自己的开发项目里使用Java批注的方法。就像我在引言里讲到的,批注已经成为现代Java框架和应用程序的重要一部分;就拿 JUnit4举个例子,Java批注已经允许JUnit的开发人员有了以更丰富的方式表示测试的方法,而不用要求测试编写者强制使用统一的命名规则。还有 Grails,这里批注可以被用来向“类似铁轨(rails-like)”的框架提供信息。批注的能力有很多,但是要记住,能力越大,责任也越大。批注是为了给开发人员提供标记信息,而不是用来隐藏运行配置。
 
完整的Java代码例子:
 
package com.shou.www;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface FormLabel {
 String label();
 int width() default 40;
}
public class AnnotationDemo {
 private String firstname="Zhao";
 private String lastname="Bacoo";
 private String postcode="116023";
 public AnnotationDemo() {
 }
 @FormLabel(label = "First Name")
 public String getFirstname() {
  return firstname;
 }
 public void setFirstname(String firstname) {
  this.firstname = firstname;
 }
 @FormLabel(label = "Last Name", width = 80)
 public String getLastname() {
  return lastname;
 }
 public void setLastname(String lastname) {
  this.lastname = lastname;
 }
 @FormLabel(label = "Postal code", width = 10)
 public String getPostcode() {
  return postcode;
 }
 public void setPostcode(String postcode) {
  this.postcode = postcode;
 }
 public void processForm(Object o) {
  for (Method m : o.getClass().getMethods()) {
   if (m.isAnnotationPresent(FormLabel.class) &&
   m.getReturnType() == String.class) {
    FormLabel formLabel = m.getAnnotation(FormLabel.class);
    try {
     String value = (String) m.invoke(o);
     String label = formLabel.label();
     int width = formLabel.width();
     System.out.printf("%s[%d]:%s\n", label, width, value);
    } catch (IllegalArgumentException ex) {
     ex.printStackTrace();
    }
    catch (IllegalAccessException ex) {
     ex.printStackTrace();
    }
    catch (InvocationTargetException ex) {
     ex.printStackTrace();
    }
   }
  }
 }
 public static void main(String[] args) {
  // TODO Auto-generated method stub
  AnnotationDemo ad=new AnnotationDemo();
  ad.processForm(ad);
 }
 
}
//程序运行的结果如下:
//First Name[40]:Zhao
//Last Name[80]:Bacoo
//Postal code[10]:116023
最后,在IBM的网站上还有翻译过来的两篇不错的关于java注释方面的讲解,但是那两篇文章比较基本,不如上面这篇文章这样经典和深刻,有兴趣的读者可以参考:
http://www.ibm.com/developerworks/cn/java/j-annotate1/
http://www.ibm.com/developerworks/cn/java/j-annotate2.html

posted on 2007-12-20 23:39 so true 阅读(457) 评论(0)  编辑  收藏 所属分类: Java


只有注册用户登录后才能发表评论。


网站导航: