32.1 Java进阶之注解概念,工作原理

本篇文章学习总结自《Java核心技术卷二》

1.什么是注解?

如果使用过Spring做开发,相信对注解并不陌生,相信大家都用过@Resource,@Autowired注解,通过将@Resource这个注解修饰一个类,Spring会自动这个类进行对象创建,通过@Autowired修饰一个变量,Spring会将这个变量赋值为相应的类的对象引用。
这样我们就能将类的对象的管理交给Spring来进行管理,方便自己的开发工作。

所以,我们可以把注解看作一种标记工具,通过这种工具,我们能开发出一些框架级别的东西来帮助我们提高编程效率。 我们可以拿它和泛型进行类比。

首先我们要知道和泛型一样,注解不会改变程序的编译方式,也就是Java编译器对于包含注解和不包含注解的代码会产生相同的虚拟机指令。但通过注解我们可以对一些开发进行简化。

1.1.如何才能使用注解?

为了我们能使用注解,我们需要选择一个处理工具,然后向我们的处理工具可以理解的代码中插入注解,之后运用该处理工具处理代码。例如Spring框架就是一个处理工具。
所以,简单来说,注解就是那些插入到源代码中使用其他工具可以对其进行处理的标签。
使用注解很简单,真正具有挑战性的是自己开发注解。

2.注解的基本概念

在Java中,注解是被当作一个修饰符来使用的,它被置于被注解项之前,中间没有分号,每一个注解的名称前面都加上了一个@符号,注解可以作为代码的一部分。 但是如果没有正确的工具去处理它,注解是不能生效的。

例如:

public class MyClass{
 @Test 
 public void checkRandomInsertions();
}

上面代码中的 @Test自身不会做任何事,它需要工具的支持才会有用。(后面会讲到)

2.1 包含元素的注解

除了类似于@Test,有的注解中也允许包含参数,我们可以传入参数的值来控制注解的处理逻辑。
例如:

@Test(timeout="10000")

包含的元素可以被读取这些注解的工具来处理,元素也允许有多种形式。

2.2 注解的作用范围

除方法外,我们还可以注解类,成员变量以及局部变量,我们还可以注解包,参数变量,类型参数以及类型用法,这些注解可以存在于任何可以放置像public ,static修饰符的地方。
我们可以把注解合理的理解为一个修饰符。

2.3 注解的定义

每个注解都必须通过一个注解接口进行定义,这些接口中的方法与注解中的元素相对应。
例如:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestSelf{
 long timeout() default 0L;
}

其中 @Target 和 @Retention 是元注解,它们注解了TestSelf注解。(@Target将TestSelf注解标识成一个只能运用到方法上的注解,@Retention将TestSelf标识为当类文件载入到虚拟机的时候,仍可以保留下来)
@interface 声明创建一个Java注解接口,处理注解的工具将接受那些实现这个注解接口的对象,这类工具可以调用timeout方法来检索某个特定Test注解的timeout元素。

2.4 元注解

元注解能让我们定义注解的作用范围,从上面我们可以知道的元注解有两个:@Target和@Retention

2.4.1 @Target

可以知道,它是定义注解的作用范围是方法级别,还是其他级别的,我们看一下ElementType类:

public enum ElementType {
    TYPE, //用于类、接口(包括注解类型)或 enum 声明 -ok
    
    FIELD, //用于成员变量(包括枚举常量)-ok
    
    METHOD,  //用于方法 -ok
    
    PARAMETER, //用于参数 -ok
    
    CONSTRUCTOR, // 用于构造方法 -ok
   
    LOCAL_VARIABLE,  // 用于局部变量 -ok
   
    ANNOTATION_TYPE,  // 用于注解类型 -ok
   
    PACKAGE,  // 用于包,不常用
  
    TYPE_PARAMETER, //用于类型参数   @since 1.8  -ok

    TYPE_USE //使用一种类型 @since 1.8 -ok
}

可以为指定注解定义多个作用范围,例如:

@Target({ElementType.METHOD})
@Target({ElementType.TYPE,ElementType.METHOD})

具体实例如下:

//考虑到代码篇幅较大,完整代码在文章底部链接中
@TypeTest
@TypeUseTest
public class GoodTest<@TypeParameterTest V> {
    @TypeUseTest
    @FiledTest
    private String field;

    @MethodTest
    public void doSomething(){
        System.out.println("哈哈哈");
    }

    public void doSomething1(@TypeUseTest @ParameterTest String param1){
        System.out.println("呵呵呵");
    }

    @TypeUseTest
    @ConstructorTest
    public GoodTest(){
        System.out.println("这是构造方法");
    }

    public void doSomething3(){
        @TypeUseTest
        @LocalVariableTest
        String local;
    }

    @TypeUseTest
    @AnnotationTypeTest
    private @interface test{

    }

    @TypeUseTest
    public int test(){
        return 1;
    }
}

2.4.2 @Retention

它是用来定义注解信息的保留级别,用于描述注解的生命周期,也就是该注解被保留的时间长短。@Retention 注解中的成员变量(value)用来设置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚举类型,RetentionPolicy 有 3 个枚举常量,如下所示:

public enum RetentionPolicy {
    SOURCE, //在源文件中有效(即源文件保留)
    CLASS, //在 class 文件中有效(即 class 保留)
    RUNTIME //在运行时有效(即运行时保留)
}

上面三者生命周期大小排序为 SOURCE < CLASS < RUNTIME。
这个需要注解根据我们自定义注解的性质去使用:
如果需要在运行时去动态获取注解信息,那只能用 RUNTIME
如果只要在编译时进行一些预处理操作,比如生成一些辅助代码(比如一些脚本信息),就用 CLASS ;
如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 。

例如下面定义了一个需要在程序运行时去获取注解信息的注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestSelf{
 long timeout() default 0L;
}

2.4.3 @Documented

这个注解用来指定修饰的注解类会被javaDoc工具提取为文档。默认情况下javaDoc是不会将注解接口抽取为文档的。
例如:

@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyDocumented {
    public String value() default "这是@Documented注解";
}
@MyDocumented
public class DocumentedTest {

    /**
     * 测试Documented注解知否生效
     * @return
     */
    @MyDocumented
    public String test(){
        return "测试";
    }
}

2.4.4 @Inherited

@Inherited 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。
实例:

public class InheritedTest {
    /**
     * 使用元注解Inherited
     */
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Inherited
    private @interface WithInherited {
    }

    /**
     * 不使用元注解Inherited
     */
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    private @interface NoInherited {
    }

    @WithInherited
    @NoInherited
    public static class SuperClass {

    }

    public static class ChildClass extends SuperClass {

    }

    public static void main(String[] args) {
        AnnotatedElement element = SuperClass.class;
        System.out.println("SuperClass 是否被 @WithInherited 注解? " + element.isAnnotationPresent(WithInherited.class));
        System.out.println("SuperClass 是否被 @NoInherited 注解? " + element.isAnnotationPresent(NoInherited.class));
        AnnotatedElement childElement = ChildClass.class;
        System.out.println("ChildClass 是否继承 SuperClass 中的 @WithInherited 注解? " + childElement.isAnnotationPresent(WithInherited.class));
        System.out.println("ChildClass 是否继承 SuperClass 中的 @NoInherited 注解? " + childElement.isAnnotationPresent(NoInherited.class));
    }

}

在这里插入图片描述

2.4.5 @Repeatable

@Repeatable 注解是 Java 8 新增加的,它允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。

例如:

/**
 * 注解类型 Repeatable 用于指明它作为元注解所声明的注解类型是可重复的.
 * 也就是用 @Repeatable 声明的注解 A, 可以作为容器注解, A 保证了被其注解的注解可以重复使用.
 */
public class RepeatableStudy {

    /**
     * 定义容器注解
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    private @interface Container {
        //这里存储注解Element的参数组
        Element[] value();
    }

    /**
     * 定义可复用注解并指向容器注解
     */
    @Repeatable(Container.class)
    private @interface Element {
        String value() default "";
    }

    /**
     * 这里的可复用注解的注解信息会被注解Container接收
     * 我们可以通过得到Container信息来获取这里的注解信息
     */
    @Element("开始")
    @Element("准备")
    @Element("出发")
    @Element("到达目的地")
    private class Running {

    }

    public static void main(String[] args) {
        //判断Running是否存储了Container注解信息
        if(Running.class.isAnnotationPresent(Container.class)) {
            Container c = Running.class.getAnnotation(Container.class);
            System.out.println("Running's Element:");
            //遍历实际的Element注解中的信息
            for(Element e: c.value()){
                System.out.println(e.value());
            }
        }
    }
}

在这里插入图片描述

2.4.6 @Native

使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。

3.了解注解基本工作原理

3.1 下面我们通过一个例子来学习注解的基本工作原理

下面有一个程序:

3.1.1 首先定义注解ActionListernFor.java

/**
 * 定义一个ActionListenerFor注解,用于监听方法的执行
 * 这个注解只能修饰方法且能在运行时获取其信息
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
   String source();
}

3.1.2 然后编写注解处理工具类(这是自定义注解的关键,没有它注解就是个摆设)

ActionListenerInstaller.java

/**
 * ActionListener注解处理工具类
 */
public class ActionListenerInstaller {

   /**
    * 这个方法为注解生效做好了前提条件
    * 主要分为两步:1.扫描出注解修饰的方法 2.将指定方法和注解生效的对象进行绑定
    * @param obj 目标对象
    */
   public static void processAnnotations(Object obj) {
      try {
         Class<?> cl = obj.getClass();//获取目标对象的Class对象

         /*
           遍历其方法信息,处理那些被ActionListener修饰的方法
          */
         for (Method m : cl.getDeclaredMethods()) {
            //检查使用ActionListenerFor注解的方法并将其加入监听列表中
            ActionListenerFor a = m.getAnnotation(ActionListenerFor.class);
            if (a != null) {
               //获取Class中名为source注解值的Field对象
               Field f = cl.getDeclaredField(a.source());
               f.setAccessible(true);
               //将方法加入监听中
               addListener(f.get(obj), obj, m);
            }
         }
      } catch(ReflectiveOperationException e){
         e.printStackTrace();
      }
   }

   /**
    * 此方法将指定的方法和指定的组件进行绑定,已达到监听的效果
    * 注意:此方法的实现用到了jdk的动态代理机制,看不懂的同学可以学习下动态代理原理
    * @param source 属性对象
    * @param param 目标对象
    * @param m 方法对象
    * @throws ReflectiveOperationException
    */
   public static void addListener(Object source, final Object param, final Method m) throws ReflectiveOperationException {

      //定义一个前置处理器,让按钮在执行完点击事件后执行指定的方法
      InvocationHandler handler = new InvocationHandler() {

         final Method realM = m;
         final Object realProxy = param;

            public Object invoke(Object proxy, Method m1, Object[] args) throws Throwable {
               System.out.println(m1.getName());
               if(m1.getName().equals("actionPerformed")){
                  System.out.println("执行actionPerformed!!");
               }
               //这个操作就是将param参数传入并执行m方法
               return realM.invoke(realProxy);
            }
         };

      // 获取监听器代理对象
      Object listenerProxy = Proxy.newProxyInstance(null,new Class[] { ActionListener.class }, handler);

      // 获取指定组件的指定方法对象
      Method adder = source.getClass().getMethod("addActionListener", ActionListener.class);

      //给指定按钮组件加入监听器
      adder.invoke(source, listenerProxy);
   }
}

3.1.3 应用代码

ButtonFrame.java

/**
 * 一个验证ActionListenerFor注解的UI面板
 */
public class ButtonFrame extends JFrame {

   private static final int DEFAULT_WIDTH = 300;
   private static final int DEFAULT_HEIGHT = 200;

   private JPanel panel;
   private JButton yellowButton;
   private JButton blueButton;
   private JButton redButton;

   public ButtonFrame() {
      setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
      panel = new JPanel();
      add(panel);
      yellowButton = new JButton("Yellow");
      blueButton = new JButton("Blue");
      redButton = new JButton("Red");
      panel.add(yellowButton);
      panel.add(blueButton);
      panel.add(redButton);

      //在这里使用注解处理器让被ActionListenerFor修饰的方法生效
      ActionListenerInstaller.processAnnotations(this);
   }

   /**
    * 这里使用注解方法的方式为名为yellowButton的按钮添加监听器
    */
   @ActionListenerFor(source = "yellowButton")
   public void yellowBackground() {
      panel.setBackground(Color.YELLOW);
   }

   @ActionListenerFor(source = "blueButton")
   public void blueBackground() {
      panel.setBackground(Color.BLUE);
   }

   @ActionListenerFor(source = "redButton")
   public void redBackground() {
      panel.setBackground(Color.RED);
   }
}

3.1.4 测试代码

/**
 * 自定义注解效果测试
 */
public class ButtonTest {
   public static void main(String[] args) {
      EventQueue.invokeLater(() -> {
            ButtonFrame frame = new ButtonFrame();
            frame.setTitle("ButtonTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
         });
   }
}

我们首先来运行一下上面程序可以看到下面结果:
在这里插入图片描述

我们可以很清楚的知道上面的程序中主要有一个图形化的界面和三个按钮,我们每点击一个按钮界面的背景颜色会发生相应的改变。

我们普通的做法是给每个按钮都注册一个事件监听器,但上面却不是,它是利用注解来完成事件响应的。

3.2 代码解析

下面我们分析下它是如何完成的

3.2.1 首先我们要定义一个注解

在上面程序中定义了一个名为ActionListenerFor的注解

package annotations;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor{
   String source();
}

处理注解的工具可以调用source()方法来检索注解中的source元素值做处理。

3.2.2 定义处理注解工具

当我们定义好一个注解后,它本身是没任何功能的,它就是一个可以标注的注解。 但是如果我们编写了针对这个注解的处理工具,我们就可以让使用工具让这个注解发挥出它的作用。
在上面的例子中ActionListenerInstaller.java 就是针对ActionListenerFor注解的一种处理工具。

它的主要作用就是让注解发挥作用,它的真正的功能其实就是让注解了的方法和按钮事件相对应,从而使我们点击按钮会有效果。

下面我们结合上面的使用注解的地方逐步分析:

  /**
 * 这里使用注解方法的方式为名为yellowButton的按钮添加监听器
 */
@ActionListenerFor(source = "yellowButton")
public void yellowBackground() {
   panel.setBackground(Color.YELLOW);
}

我们可以看到,上面的例子中,我们使用了ActionListenerFor修饰了方法yellowBacgroud,在这里我们只是标注了这个方法,那么如何让注解生效呢?

通过分析代码我们可以知道,真正让注解生效的是:

ActionListenerInstaller.processAnnotations(this);

**processAnnotations()方法的主要作用就是获取上面的三个注解了的方法,并且将这几个方法自动的注册到监听器中,从而实现了三个按钮的监听。**具体细节大家可以仔细看一下代码。

下图展示了上面例子中注解是如何被处理的:
在这里插入图片描述

可能大家不容易get到上面图的重点,下面是我针对上面图中程序运用API,反射机制处理注解的详细流程解释:
在这里插入图片描述

4 完整代码地址

Java基础学习/src/main/java/Progress/exa32_1 · 严家豆/Study - 码云 - 开源中国 (gitee.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小牧之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值