在编Java程序的时候,我们经常会碰到annotation。比如:@Override 我们在子类继承父类的时候,会经常用到这个annotation。它告诉编译器这个方法是override父类的方法的。@WebServlet("/myservlet") 在进行Web开发的时候,我们用这个annotation表示这个类是一个servlet。Web容器会识别这个annotation,在运行的时候调用它。很多人说annotation是注释,初看起来有一点像,它对程序的编写和编译似乎没有什么影响,只是给人看的一个对程序的附注。从这点上,确实有一点像注释。不过,它跟注释不同的是,它会影响程序的运行。比如,上面提到的@Override,如果没有override父类的方法,编译器会给出错误提示;再比如,上面的@WebServlet,如果没有这个注解,程序是运行不起来的。

  由此看来,annotation不是注释,注释是给人看的,并不影响程序的编译和运行时候的行为。annotation其实不是给人看的,那么它是给谁看的呢?它被设计出来,用于给另外的程序看的,比如编译器,比如框架,比如Web容器。

  这些外在的程序通过某种方式查看到这些annotation后,就可以采取相应的行为。具体解释一下。

  假如我们要做一个Web容器,类似于Tomcat这种的。它的一个基本功能就是加载servlet。按照JavaEE的规范,容器需要管理servlet的生命周期,第一件事情就是要识别哪些类是servlet。那么,容器启动的时候,可以扫描全部类,找到包含@WebServlet注解的,识别它们,然后加载它们。那么,这个@WebServlet注解就是在运行时起作用的,Java里面把它的作用范围规定为RUNTIME。再看@Override,这个是给编译器看的,编译程序读用户程序的源代码,识别出有override注解的方法,就去检查上层父类相应方法。这个@Override注解就是在编译的时候起作用的,编译之后,就不存在了。Java里面把它的作用范围规定为SOURCE。类似的注解还有@Test,程序员写好了程序,想交给测试框架去测试自己写的方法,就可以用这个注解。测试框架会读取源代码,识别出有@Test注解的方法,就可以进行测试了。

  接下来,我们自己动手做一个注解看看效果加深理解。我们想做的例子是一个运行时框架加载别的客户类,并运行其中的初始化方法。作为框架,我们可以提供一个@InitMethod注解给客户程序员。客户类代码如下:

public class InitDemo {

    @InitMethod
    public void init(){
        System.out.println("init ...");
    }
}

客户类程序员在init()方法上标注了@InitMethod注解,声明这就是本类的初始化方法。框架程序识别它,并调用它。

接下来我们看怎么提供这个注解的实现。代码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitMethod {
}

第一次看到这个注解的实现的时候,人们都会大吃一惊,觉得很像是在定义一个 interface。的确很像,Java5之后,提供了这样的手段,让人定义注解。上面就声明了有一个叫InitMethod的注解,它是修饰方法的,在运行时可见。问题来了,上面这一段并不是真正的实现,只是一个定义或者声明。那真正的实现怎么做呢?这些当然要定义者来提供实现的。我们作为框架程序的作者,有责任实现它,代码如下:

public class InitProcessor {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,IllegalArgumentException,InvocationTargetException, InstantiationException {
        Class<?> clazz = Class.forName("InitDemo");
        Method[] methods = clazz.getMethods();
        if(methods!=null){
            for(Method method:methods){
                boolean isInitMethod = method.isAnnotationPresent(InitMethod.class);
                if(isInitMethod){
                    method.invoke(clazz.newInstance(), null);
                }
            }
        }
    }
}

  稍微解释一下上面的代码。

  为了从客户类InitDemo里面读出annotation信息,需要用到reflection机制。先通过Class.forName()加载类拿到Class信息;然后通过getMethods()拿到所有public的方法(包含从上层父类继承下来的公共方法);接下来是重点: method.isAnnotationPresent(InitMethod.class),这一行判断一个方法是否标记为InitMethod;如果是,则创建一个对象并调用。这样在框架中实现了对类的初始化方法进行调用。运行上面的程序,就能看到确实调用了初始化方法。我们的annotation工作了。Annotation基本的使用就是这样的,一点也不神秘。

  下面介绍更多的一些特性。Annotation的基本定义如下:

@Target(ElementType.xxxxxx)
@Retention(RetentionPolicy.xxxxxx)
[Access Specifier] @interface <AnnotationName> {         
   DataType <Method Name>() [default value];
}

  Annotation里面的Method其实是客户程序在使用该annotation的时候的参数定义,如@WebServlet(urlPatterns=”/abc”,loadOnStartup=1),其中urlPatterns和loadOnStartup就是参数,定义的时候用类似于方法定义的方式。如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServlet {         
String urlPatterns() default "/";  
int loadOnStartup() default 0; 
}

 这种定义方式也有点让人费解,对使用者有些费解。不过从实现者的角度,倒是正常。因为实现注解的程序员是这么写的,代码如下:

Annotation annotation=clazz.getAnnotation(WebServlet.class);
WebServlet ws = (WebServlet)annotation;
System.out.println(ws.loadOnStartup());

这一下子就可以看出来了,实现者确实是当成方法调用的。

@Target指定该注解针对的地方,有几种ElementType:

    ElementType.TYPE,         - 类、接口 
    ElementType.FIELD,         - 字段
    ElementType.METHOD,        - 方法
    ElementType.PARAMETER,      - 参数
    ElementType.CONSTRUCTOR,     - 构造方法
    ElementType.LOCAL_VARIABLE,   - 局部变量
    ElementType.ANNOTATION_TYPE,   - 注解
    ElementType.PACKAGE       - 包

@Retention指定注解的保留域,有三种RetentionPolicy:

  RetentionPolicy.SOURCE,            - Annotation信息仅存在于源代码级别,由编译器处理,处理完之后就没有保留了
  RetentionPolicy.CLASS,             - Annotation信息保留于类对应的.class文件中
  RetentionPolicy.RUNTIME            - Annotation信息保留于class文件中,并且可由JVM读入,运行时使用

  SOURCE选项常用于代码自动生成。RetentionPolicy里的CLASS选项让人不好理解。它的含义是说在编译的CLASS文件中记录了这个注解,但是JVM获取不到。那这有什么用处?这确实是一种很少见的应用场景,比如一些直接通过bytecode方式运行的程序,如ASM,它直接读CLASS字节码,并不通过JVM去装载class,这个情况下就需要用到这个选项。不过有的编译器会把CLASS处理成RUNTIME,通过Reflection一样可见,因此有人宣称两者一样的,事实上这不符合Java官方文档说明。我们普通应用程序的开发工作中,CLASS选项用得极少,一般情况下,就用RUNTIME选项。

讲解了这些之后,我们可以把上面的例子完整写下来。

InitMethod.java

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitMethod {
    String flag() default "1";

InitDemo.java

public class InitDemo {
    @InitMethod(flag="1")
    public void init(){
        System.out.println("init ...");
    }
}

InitProcessor.java

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class InitProcessor{
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
        Class<?> clazz = Class.forName("InitDemo");
        Method[] methods = clazz.getMethods();
        if(methods!=null){
            for(Method method:methods){
                boolean isInit = method.isAnnotationPresent(InitMethod.class);
                if(isInit){
                    method.invoke(clazz.newInstance(), null);
                    Annotation annotation=method.getAnnotation(InitMethod.class);
                    InitMethod im = (InitMethod)annotation;
                    System.out.println(im.flag());
                }
            }
        }
    }
}

  InitProcessor相当于一个框架,对客户程序进行管理并自动调用初始化方法。而站在客户化程序员的角度,就是要遵守某种协议规范,写的程序就能按照预想的正确运行起来,简单易用,自己少写很多代码。不过“一物有二柄”,凡事都有一个优缺点。这么写程序,对客户程序员来讲,获得简单的同时,却丢失了全局观。很多程序员都不知道这些程序究竟是怎么运行起来的,有点失控的感觉。这也是学习框架过程中最经常的困惑。当代编程的范式,已经全部转换成基于框架的编程了。大部分人都面临着知其然而不知其所以然的问题。这种情况下,D.I.Y.,自动动手实现某种机制就显得格外重要。

  为了加深对annotation的了解,我们再试着编写一个自动生成代码的例子。我们想做这么一件事情,模仿JUnit,应用程序员写了一个类,我们自动生成测试类。这个就可以使用到作用于SOURCE级别的annotation来实现。我们先提供一个@UnitTest注解给客户程序员使用。这个注解的作用就是自动生成一个测试类,把客户程序里面的方法都调用一次。

同样的,我们先定义注解,代码如下(UnitTest.java):

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface UnitTest {
  String prefix() default "Test";
}

这个定义没有什么特别的,跟上述讲解的不同在于使用了RetentionPolicy.SOURCE,表示这个注解作用于源代码层面,在编译时使用。

使用层面就更加简单了,代码如下(Tool.java):

@UnitTest
public class Tool {
    public void check(){
    System.out.println("check");
    }
}

根据规定,对这类注解的实现类要继承AbstractProcessor抽象类,这个抽象类是由javax里面给出的,所以需要先import javax.annotation.processing.AbstractProcessor;

在类里面,主要是要override 一个方法:process(),好,我们直接看代码(UnitTestProcessor.java):

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.JavaFileObject;
import javax.lang.model.SourceVersion;

@SupportedAnnotationTypes({"UnitTest"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class UnitTestProcessor extends AbstractProcessor{
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for(Element clz: roundEnv.getElementsAnnotatedWith(UnitTest.class)){
          UnitTest ut = clz.getAnnotation(UnitTest.class);

          String newFileStr = "public class "+ut.prefix() + clz.getSimpleName()+" {\n\n";

          //add constructor
          newFileStr += "public " + ut.prefix() + clz.getSimpleName() + "() {\n";
          newFileStr+="}\n\n"; //end of constructor

          //add main()
          newFileStr += "public static void main(String[] args) {\n";

          //new instance
          newFileStr += clz.getSimpleName() + " clz = new "+clz.getSimpleName()+"();\n";

          //add test method
          for(Element testmethod : clz.getEnclosedElements()){ 
            if(!testmethod.getSimpleName().toString().equals("<init>") && testmethod.asType() instanceof ExecutableType){ //add test method
              newFileStr += "clz."+testmethod.getSimpleName()+"();\n";
            }
          }

          newFileStr+="}\n"; //end of main()
          newFileStr+="}\n"; //end of class

          try {
            JavaFileObject jfo = processingEnv.getFiler().createSourceFile(ut.prefix() + clz.getSimpleName(), clz);
            Writer writer = jfo.openWriter();
            writer.append(newFileStr);
            writer.flush();
            writer.close();
          } catch (IOException ex) {
          }
        }

        return true;
      }

    }

  把这个程序解释一下:

  Process()带两个参数:the set of annotations that is being processed in this round, and a RoundEnv reference that contains information about the current processing round.一个是要处理的annotation集合,一个是环境参数。原理上process()的主体是对集合中的每一个annotation进行处理。处理之后新生成的一些程序或许还会包含有同样的annotation,就需要递归处理,所以有一个round的概念,需要一轮一轮处理完毕。不过,我们这个是教学,只是为了演示,我们就简化处理。通过roundEnv.getElementsAnnotatedWith(UnitTest.class),我们拿到包含有UnitTest注解的全部类,因为@UnitTest是作用于类之上的。通过clz.getAnnotation(UnitTest.class),我们可以把类上的注解拿到,然后从注解中获取信息。我们的任务比较简单,只是要生成一个测试类,所以我们用newFileStr拼字符串,写这个文件。先写类定义:newFileStr += "public class "+ut.prefix() + clz.getSimpleName()+" {\n\n"如果类的名字叫Tool,那么我们自动生成的测试类就叫TestTool.然后定义构造函数newFileStr += "public " + ut.prefix() + clz.getSimpleName() + "() {\n"再写main(),作为测试类的入口      newFileStr += "public static void main(String[] args) {\n"main()里面的结构,就是新建对象,然后调用方法

newFileStr += clz.getSimpleName() + " clz = new "+clz.getSimpleName()+"();\n";检查类里面的每一个element,挑出普通方法,进行调用

for(Element testmethod : clz.getEnclosedElements()){ 
   if(!testmethod.getSimpleName().toString().equals("<init>") && testmethod.asType() instanceof ExecutableType){ //add test method
      newFileStr += "clz."+testmethod.getSimpleName()+"();\n";
   }
}

程序片段中,clz.getEnclosedElements()用来找出类里面包含的elements,包括了构造函数,方法,属性定义等等。我们简化处理,只是派出了构造函数。

程序字符串准备好之后,就直接写文件:

JavaFileObject jfo = processingEnv.getFiler().createSourceFile(ut.prefix() + clz.getSimpleName(), clz);
Writer writer = jfo.openWriter();
writer.append(newFileStr);

注:

自定义annotation不困难。不过在build的时候要费一点功夫。因为这个是影响编译的行为,所以要向编译器javac声明:

先编译好UnitTestProcessor,然后

javac -processor UnitTestProcessor Tool.java

如果是IDE环境,就要进行配置,如在eclipse中,要在Project Properties ->Java Compiler->Annotation Processing中启用annotation processing,并在Factory Path中把上述编译好的处理器Jar包登记进来,并选择run这个jar包中包含的哪个processor.

因此,我们就要先打一个jar包。新建一个处理器工程,包含UnitTest.java, UnitTestProcessor.java, 还要准备一个meta文件,在工程的resources目录下建一个文件:META-INF/services/javax.annotation.processing.Processor,文件中写上处理器的名字:UnitTestProcessor。

这个独立的jar包的作用就是为客户程序员自动生成测试类代码,这是一个有用的工具包。

有了这个jar包之后,在客户工程里面引入即可。

Build项目的时候,自动生成了测试类,代码如下(TestTool.java):

public class TestTool {
public TestTool() {
}
public static void main(String[] args) {
Tool clz = new Tool();
clz.check();
}
}

  大功告成。到此,我们就能看到,用了Annotation技术,我们能做很多非平凡的工作。自己写工具,写框架,都会有一些头绪。

  作为学习者,最先了解的是Annotation的概念,学习使用现成的annotation,这是第一步;接下来就要自己写RUNTIME类型的annotation,实现一些框架的效果;进一步就是自己写SOURCE类型的annotation,提供各种源代码级别的工具。学习的进路,就这么一步步深入下去。掌握了后,就有拨开丛林,见到本尊的愉悦,一种获得知识的愉悦感。耳边总是传来“不要重新造轮子”的声音,但是,对于学习者,就应该重新造轮子。只有在学习重新造轮子的过程中,我们才能更加深刻理解技术概念。

关于Annotation注解的理解的更多相关文章

  1. [1] 注解(Annotation)-- 深入理解Java:注解(Annotation)基本概念

    转载 http://www.cnblogs.com/peida/archive/2013/04/23/3036035.html 深入理解Java:注解(Annotation)基本概念 什么是注解(An ...

  2. 【JAVA - 基础】之Annotation注解浅析

    注解在JAVA中,尤其是一些ORM框架(如Hibernate等)中是比较常用的一种机制. 注解是JAVA 1.5之后引入的新功能,正确来说是反射的一部分,没有反射,注解也就无法正常使用.注解可以理解成 ...

  3. Android开发学习之路--Annotation注解简化view控件之初体验

    一般我们在写android Activity的时候总是会在onCreate方法中加上setContentView方法来加载layout,通过findViewById来实现控件的绑定,每次写这么多代码总 ...

  4. Java Annotation 注解

    java_notation.html div.oembedall-githubrepos { border: 1px solid #DDD; list-style-type: none; margin ...

  5. Hibernate的Annotation注解

    当项目变得比较大的时候,如何还使用hbm.xml文件来配置Hibernate实体就会变得比较复杂.这里Hibernate提供了Annotation注解方式,使得Hibernate的映射文件变得很方便管 ...

  6. Java基础笔记 – Annotation注解的介绍和使用 自定义注解

    Java基础笔记 – Annotation注解的介绍和使用 自定义注解 本文由arthinking发表于5年前 | Java基础 | 评论数 7 |  被围观 25,969 views+ 1.Anno ...

  7. paip.Java Annotation注解的作用and 使用

    paip.Java Annotation注解的作用and 使用 作者Attilax 艾龙,  EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://blog. ...

  8. hibernate annotation注解方式来处理映射关系

    在hibernate中,通常配置对象关系映射关系有两种,一种是基于xml的方式,另一种是基于annotation的注解方式,熟话说,萝卜青菜,可有所爱,每个人都有自己喜欢的配置方式,我在试了这两种方式 ...

  9. java EE中的hello1.java及Annotation(注解)

    一.Annotation(注解) 注解(Annotation)很重要,未来的开发模式都需要注解,注解是java.lang.annotation包,Annotation是从java5引入的,它提供一些不 ...

随机推荐

  1. PTA 带头结点的链式表操作集

    6-2 带头结点的链式表操作集 (20 分)   本题要求实现带头结点的链式表操作集. 函数接口定义: List MakeEmpty(); Position Find( List L, Element ...

  2. Android学习之异步消息处理机制

    •前言 我们在开发 APP 的过程中,经常需要更新 UI: 但是 Android 的 UI 线程是不安全的: 如果想更新 UI 线程,必须在进程的主线程中: 这里我们引用了异步消息处理机制来解决之一问 ...

  3. PAT (Basic Level) Practice (中文) 1050 螺旋矩阵 (25 分) 凌宸1642

    PAT (Basic Level) Practice (中文) 1050 螺旋矩阵 (25 分) 目录 PAT (Basic Level) Practice (中文) 1050 螺旋矩阵 (25 分) ...

  4. Nacos概述及安装

    Nacos是什么? 在Spring Cloud中我们使用eureka.consul等做为服务注册中心,使用Spring Cloud Config做为配置中心.而Spring Cloud中,也可以使用n ...

  5. 13个精选的React JS框架

    如果你正在使用 React.js 或 React Native 创建用户界面,可以试一试本文推荐的这些框架. React.js 和 React Native 是流行的用户界面(UI)开发平台,且都是开 ...

  6. python基础(十二):if分支表达式

    有时候,我们需要依照某种条件,再决定要不要做某个操作.在Python中,if语句能够帮助我们检查程序的当前状态,告诉计算机接下来该做什么. 条件表达式 每个if后面都跟着一个True或False的表达 ...

  7. 目标检测性能评价——关于mAP计算的思考

    1. 基本要求 从直观理解,一个目标检测网络性能好,主要有以下表现: 把画面中的目标都检测到--漏检少 背景不被检测为目标--误检少 目标类别符合实际--分类准 目标框与物体的边缘贴合度高-- 定位准 ...

  8. Java8中的Stream流式操作 - 入门篇

    作者:汤圆 个人博客:javalover.cc 前言 之前总是朋友朋友的叫,感觉有套近乎的嫌疑,所以后面还是给大家改个称呼吧 因为大家是来看东西的,所以暂且叫做官人吧(灵感来自于民间流传的四大名著之一 ...

  9. Java刷题-list

    一.打印两个有序链表的公共部分 补充一个关于节点的链表构造方法 Node next是设置指针域 import java.io.IOException;这个是报错信息 这是两个lO流 import ja ...

  10. CentOS系统安装Nginx

    目录 1. 官网下载地址 2. 上传到服务器安装 2.1 检查是否安装以下软件包 2.2 安装 2.3 安装nginx 3. 启动&停止 nginx是 HTTP 和反向代理服务器,邮件(IMA ...