暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

万字长文带你详聊Java注解本质

30

写在文章开头

注解也叫元数据,在jdk1.5引入,和类、接口等属于同一个层级。也可用在类、字段、接口、方法、形参上。 初学者可能会把注解和注释混淆,实际上注释是告知告知程序员这段程序的用意,而注解则是告知计算机这段程序的用意。

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

注解的作用

生成带有说明的文档

如下所示代码,由于添加了作者、版本、since
这些注解,所以在生成文档的时候就会体现这些内容。

/**
 * 注解javadoc演示
 *
 * @author test
 * @version 1.0
 * @since 1.5
 */

public class AnnoDemo1 {

    /**
     * 计算两数的和
     * @param a 整数
     * @param b 整数
     * @return 两数的和
     */

    public int add(int a, int b ){
        return a + b;
    }
}

使用javadoc
命令进行文档生成

可以看到生成的类文档的作者、版本等都是我们注解后面编写的值。

代码分析

使用反射完成基于代码里标识的注解,对代码进行分析,从而完成动态增强,这也是Spring
框架对注解最典型的运用(因为示例篇幅较大,代码分析的例子会在下文给出)。

编译检查

通过代码里标识的注解让编译器能够实现基本的编译检查,最典型的就是Override
注解。

public class MsgEventFactory implements EventFactory<MessageModel{
    @Override
    public MessageModel newInstance() {
        return new MessageModel();
    }
}

java内置的三大常见注解

@Override

源码如下所示,可以看到元注解有target
Retention
,其中Retention
source
,即在编译时
检查当前子类重写的方法在父类中是否存在,如果存在则编译通过,反之报错。

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

@Deprecated

这个注解常用于提醒开发被加上注解的方法已经不推荐使用了。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

例如hutool
下的excel
工具类就有过期的方法,从注释即可看到官方推荐的新用法,这也是我们为什么希望程序员多看源码及注释的原因。

/**
  * Sax方式读取Excel07
  * 
  * @param in 输入流
  * @param rid Sheet rid,-1表示全部Sheet, 0表示第一个Sheet
  * @param rowHandler 行处理器
  * @return {@link Excel07SaxReader}
  * @since 3.2.0
  * @deprecated 请使用 {@link #readBySax(InputStream, int, RowHandler)}
  */

 @Deprecated
 public static Excel07SaxReader read07BySax(InputStream in, int rid, RowHandler rowHandler) {
  try {
   return new Excel07SaxReader(rowHandler).read(in, rid);
  } catch (NoClassDefFoundError e) {
   throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);
  }
 }

@SuppressWarnings

因为某些原因导致编码在编译时告警,通过该注解即可压制程序中某些警告(即IDEA不报黄)。

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {

    String[] value();
}

什么是元注解?它是用来做什么的?

简介

除了直接使用JDK 定义好的注解,我们还可以自定义注解,在JDK 1.5中提供了4个标准的用来对注解类型进行注解的注解类,我们称之为 meta-annotation(元注解),他们分别是:

  1. @Target
  2. @Retention
  3. @Documented
  4. @Inherited

@Target

Target
用于描述注解能够作用的位置,有3个值,可以通过ElementType
获取。

ElementType
取值:

  1. TYPE
    :可以作用于类上。
  2. METHOD
    :可以作用于方法上。
  3. FIELD
    :可以作用于成员变量上。

如下注解 只可作用于类和字段上,在函数上则会报错:

@Target({ElementType.TYPE,ElementType.FIELD})
public @interface MyAnno3 {
}


如下所示,我们将Target
在字段上的注解用在方法上就报错了。

@Retention

描述注解被保留的阶段,例如 @Retention(RetentionPolicy.RUNTIME)
即表示当前被描述的注解,会保留到class
字节码文件中,并被JVM
读取到。

@Documented

描述注解是否被抽取到接口文档中,这里我们举个例子演示一下,我们自定义注解Myanno3
,如下代码,Myanno3
加入注解@Documented
后,worker
类使用该Myanno3
注解,当使用worker
生成文档后该注解会被显示在使用注解的类、函数、字段上:



import java.lang.annotation.*;

/**

 元注解:用于描述注解的注解
     * @Target:描述注解能够作用的位置
     * @Retention:描述注解被保留的阶段
     * @Documented:描述注解是否被抽取到api文档中
     * @Inherited:描述注解是否被子类继承


 *
 */


@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnno3 {
}


Worker
上使用MyAnno
注解,可以看到我们在类、字段、方法上都用到了注解。


@MyAnno(value=12,per = Person.P1,anno2 = @MyAnno2,strs="bbb",name = "李四")
@MyAnno3
public class Worker {
    @MyAnno3
    public String name = "aaa";
    @MyAnno3
    public void show(){


    }
}


使用javadoc
命令生成文档后,可以看到该类的myAnno3
的注解都存在:

@Inherited

描述注解是否被子类继承

基于注解实现简单的测试框架

需求描述

我们希望自定义一个注解check
,通过check
注解获取我们编写方法并执行,查看其是正常输出还是异常。

定义注解

首先定义check
注解

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

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


实现计算类

需要测试的类代码,逻辑比较简单,就是一些简单的计算。

/**
 * 计算类
 */

public class Calculator {

    /**
     * 加法,存在空指针异常
     */

    @Check
    public void add() {
        String str = null;
        str.toString();
        System.out.println("1 + 0 =" + (1 + 0));
    }


    //减法
    @Check
    public void sub() {
        System.out.println("1 - 0 =" + (1 - 0));
    }

    //乘法
    @Check
    public void mul() {
        System.out.println("1 * 0 =" + (1 * 0));
    }

    /**
     * 除法 被除数为0
     */

    @Check
    public void div() {
        System.out.println("1 / 0 =" + (1 / 0));
    }


}



基于check注解反射调用

因为我们对方法指明了注解,所以在执行时可以根据注解的存在判断当前方法是否需要检查,对于需要检查的方法我们直接通过反射的方式完成调用,若报错则将信息写入error
日志中。

public class CheckUtil {


    public static void main(String[] args) throws IOException {
        Calculator calculator = new Calculator();
        //反射获取calculator的方法
        Class<? extends Calculator> clzz = calculator.getClass();
        Method[] methods = clzz.getMethods();
        String methodName = null;


        int errCount = 0;
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("err.log"))) {
            for (Method method : methods) {
                methodName = method.getName();
                //判断是否有check注解若有则反射调用
                if (method.isAnnotationPresent(Check.class)) {
                    try {
                        method.invoke(calculator);
                    } catch (Exception e) {
                        //有报错则将错误写入日志
                        String msg = String.format("出错了,第%d个错误,方法名:%s,错误原因:%s", ++errCount, methodName, e.getCause().getMessage());
                        System.out.println(msg);
                        writer.write(msg);
                        writer.flush();
                        writer.newLine();
                    }
                }
            }
        }


    }
}


错误日志输出结果:

出错了,第1个错误,方法名:add,错误原因:null
出错了,第2个错误,方法名:div,错误原因:/ by zero


注解的本质是什么呢?

聊到注解的本质,其实最简单的方法就是反编译看看实质,代码如下所示:

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Foo{
String[] value();
boolean bar();
}

使用以下命令完成编译生成字节码还反编译java
文件

 javac Foo.java

javap -c Foo.class

我们就可以得出如下一段输出,可以看到看注解的本质就是一个接口。

Compiled from "Foo.java"
public interface com.shark.wiki.interview.javaBase.annotation.Foo extends java.lang.annotation.Annotation {
public abstract java.lang.String[] value();

public abstract boolean bar();
}

注解与反射的关系原理实践

其实我们在日常使用Spring
框架时经常会用到注解,例如Service("userSerivice")
,那么请问Spring
是如何通过注解拿到这个bean
的值的呢? 我们不妨自定义一个注解来实验这个问题,首先我们自定义一个service

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
String value();
String scope();
}

然后我们编写如下一段代码,获取注解的value
scope
,执行前在idea
vm option
键入这一一段jvm
命令-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
生成动态代理的class
文件。

@Service(value = "userService", scope = "singleton")
public class Test {


public static void main(String[] args) throws Exception{
Service service = Test.class.getAnnotation(Service.class);
System.out.println(service.value());
System.out.println(service.scope());

}
}

在查看项目文件中会出现下图这样一个名为proxy
class
文件文件,不难猜出注解的运行时会对被注解的类生成一个动态代理。

可以得出获取注解时,Java
会为注解生成一个代理类,为scope
value
生成一个方法。在静态代码快中使用反射初始化将值赋值给m3、m4
,后续我们需要获取值时直接通过m3
m4
invoke
方法获取值。

public final class $Proxy1 extends Proxy implements Service {
   .....
    private static Method m4;
   ...
    private static Method m3;

    public $Proxy1(InvocationHandler var1) throws  {
        super(var1);
    }

   

    public final String scope() throws  {
        try {
            return (String)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }



    public final String value() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

 // 静态代码块中完成scope和value的方法的初始化
    static {
        try {
          ......
            m4 = Class.forName("com.guide.keyword.rewrite.Service").getMethod("scope");
          ......
            m3 = Class.forName("com.guide.keyword.rewrite.Service").getMethod("value");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

scope
为例,我们看到了这样一段代码,不难看出我们之前调用的Service.scope()
就是使用这个方法。这里有个invoke
,我们点进去看看调用

public final String scope() throws  {
try {
return (String)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

这时候我们看到一个接口,没有看到具体实现,没关系,源码的设计者命名永远是合理的,所以,我们完完全全可以通过查找与注解命名相关的继承类。

这时候笔者就发现了这个

具体实现源码如下,关键笔者都在代码中注释了:

public Object invoke(Object var1, Method var2, Object[] var3) {
  //var2就是上一步传入的scope
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class{
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            //根据方法名的hashCode决定var7的值。
            switch(var4.hashCode()) {
            
                ........
            }
  //根据var2标记var7,然后从返回响应的var值,这里scope就走最后一个分支了返回了scope的字符串
            switch(var7) {
           .......
            default:
            //最终返回我们的设置的字符串var6 
                Object var6 = this.memberValues.get(var4);
               .......

                    return var6;
                }
            }
        }
    }

小结

我是sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号:写代码的SharkChili,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考

java注解的基本原理:https://juejin.cn/post/6844903636733001741#heading-0


文章转载自写代码的SharkChili,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论