百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

这一篇 Java 注解,写得太好了(java中的注解)

cac55 2024-09-20 12:42 58 浏览 0 评论

前言

Java注解是在JDK1.5被引入的技术,配合反射可以在运行期间处理注解,配合apt tool可以在编译器处理注解,在JDK1.6之后,apt tool被整合到了javac里面。

什么是注解

注解其实就是一种标记,常常用于代替冗余复杂的配置(XML、properties)又或者是编译器进行一些检查如JDK自带的Override、Deprecated等,但是它本身并不起任何作用,可以说有它没它都不影响程序的正常运行,注解的作用在于「注解的处理程序」,注解处理程序通过捕获被注解标记的代码然后进行一些处理,这就是注解工作的方式。

在java中,自定义一个注解非常简单,通过@interface就能定义一个注解,实现如下

public @interface PrintMsg {}

写个测试类给他加上我们写的这个注解吧

@PrintMsgpublic class AnnotationTest {    public static void main(String[] args) {        System.out.println("annotation test OK!");    }}

我们发现写与不写这个注解的效果是相同的,这也印证了我们说的注解只是一种「标记」,有它没它并不影响程序的运行。

元注解

在实现这个注解功能之前,我们先了解一下元注解。

元注解:对注解进行注解,也就是对注解进行标记,元注解的背后处理逻辑由apt tool提供,对注解的行为做出一些限制,例如生命周期,作用范围等等。

@Retention

用于描述注解的生命周期,表示注解在什么范围有效,它有三个取值,如下表所示:

类型作用SOURCE注解只在源码阶段保留,在编译器进行编译的时候这类注解被抹除,常见的@Override就属于这种注解CLASS注解在编译期保留,但是当Java虚拟机加载class文件时会被丢弃,这个也是@Retention的「默认值」。@Deprecated和@NonNull就属于这样的注解RUNTIME注解在运行期间仍然保留,在程序中可以通过反射获取,Spring中常见的@Controller、@Service等都属于这一类

@Target

用于描述注解作用的「对象类型」,这个就非常多了,如下表所示:

类型作用的对象类型TYPE类、接口、枚举FIELD类属性METHOD方法PARAMETER参数类型CONSTRUCTOR构造方法LOCAL_VARIABLE局部变量ANNOTATION_TYPE注解PACKAGE包TYPE_PARAMETER1.8之后,泛型TYPE_USE1.8之后,除了PACKAGE之外任意类型

@Documented

将注解的元素加入Javadoc中

@Inherited

如果被这个注解标记了,被标记的类、接口会继承父类、接口的上面的注解

@Repeatable

表示该注解可以重复标记

注解的属性

除了元注解之外,我们还能给注解添加属性,注解中的属性以无参方法的形式定义,方法名为属性名,返回值为成员变量的类型,还是以上述注解为例:

首先给这个注解加亿点点细节,生命周期改为Runtime,使得运行期存在可以被我们获取

@Retention(RetentionPolicy.RUNTIME)public @interface PrintMsg {    int count() default 1;    String name() default "my name is PrintMsg";}@PrintMsg(count = 2020)public class AnnotationTest {    public static void main(String[] args) {        //通过反射获取该注解        PrintMsg annotation = AnnotationTest.class.getAnnotation(PrintMsg.class);        System.out.println(annotation.count());        System.out.println(annotation.name());    }}

输出如下:

2020my name is PrintMsg

到这里就有两个疑问了:

  1. getAnnotation获取到的是什么?一个实例?注解是一个类?
  2. 我们明明调用的是count(),name(),但是为什么说是注解的属性?

等下聊

到底什么是注解?

按照注解的生命周期以及处理方式的不同,通常将注解分为「运行时注解」「编译时注解」

  • 运行时注解的本质是实现了Annotation接口的特殊接口,JDK在运行时为其创建代理类,注解方法的调用实际是通过AnnotationInvocationHandler的invoke方法,AnnotationInvocationHandler其中维护了一个Map,Map中存放的是方法名与返回值的映射,对注解中自定义方法的调用其实最后就是用方法名去查Map并且放回的一个过程
  • 编译时注解通过注解处理器来支持,而注解处理器的实际工作过程由JDK在编译期提供支持,有兴趣可以看看javac的源码

运行时注解原理详解

之前我们说注解是一种标记,只是针对注解的作用而言,而Java语言层面注解到底是什么呢?以JSL中的一段话开头

?

An annotation type declaration specifies a new annotation type, a special kind of interface type. To distinguish an annotation type declaration from a normal interface declaration, the keyword interface is preceded by an at-sign (@).

?

简单来说就是,注解只不过是在interface前面加了@符号的特殊接口,那么不妨以PrintMsg.class开始来看看,通过javap反编译的到信息如下:

public interface com.hustdj.jdkStudy.annotation.PrintMsg extends java.lang.annotation.Annotation  minor version: 0  major version: 52  flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION  this_class: #1                          // com/hustdj/jdkStudy/annotation/PrintMsg  super_class: #3                         // java/lang/Object  interfaces: 1, fields: 0, methods: 2, attributes: 2Constant pool:   #1 = Class              #2             // com/hustdj/jdkStudy/annotation/PrintMsg   #2 = Utf8               com/hustdj/jdkStudy/annotation/PrintMsg   #3 = Class              #4             // java/lang/Object   #4 = Utf8               java/lang/Object   #5 = Class              #6             // java/lang/annotation/Annotation   #6 = Utf8               java/lang/annotation/Annotation   #7 = Utf8               count   #8 = Utf8               ()I   #9 = Utf8               AnnotationDefault  #10 = Integer            1  #11 = Utf8               name  #12 = Utf8               ()Ljava/lang/String;  #13 = Utf8               my name is PrintMsg  #14 = Utf8               SourceFile  #15 = Utf8               PrintMsg.java  #16 = Utf8               RuntimeVisibleAnnotations  #17 = Utf8               Ljava/lang/annotation/Retention;  #18 = Utf8               value  #19 = Utf8               Ljava/lang/annotation/RetentionPolicy;  #20 = Utf8               RUNTIME{  public abstract int count();    descriptor: ()I    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT    AnnotationDefault:      default_value: I#10  public abstract java.lang.String name();    descriptor: ()Ljava/lang/String;    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT    AnnotationDefault:      default_value: s#13}SourceFile: "PrintMsg.java"RuntimeVisibleAnnotations:  0: #17(#18=e#19.#20)

从第一行就不难看出,注解是一个继承自Annotation接口的接口,它并不是一个类,那么getAnnotation()拿到的到底是什么呢?不难想到,通过动态代理生成了代理类,是这样的嘛?通过启动参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true或者在上述代码中添加:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");将通过JDK的proxyGenerator生成的代理类保存下来在com.sun.proxy文件夹下面找到这个class文件,通过javap反编译结果如下:

public final class com.sun.proxy.$Proxy1 extends java.lang.reflect.Proxy implements com.hustdj.jdkStudy.annotation.PrintMsg

可以看出JDK通过动态代理实现了一个类继承我们自定义的PrintMsg接口,由于这个方法字节码太长了,看起来头疼,利用idea自带的反编译直接在idea中打开该class文件如下:

public final class $Proxy1 extends Proxy    implements PrintMsg{    public $Proxy1(InvocationHandler invocationhandler)    {        super(invocationhandler);    }    public final boolean equals(Object obj)    {        try        {            return ((Boolean)super.h.invoke(this, m1, new Object[] {                obj            })).booleanValue();        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final String name()    {        try        {            return (String)super.h.invoke(this, m3, null);        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final String toString()    {        try        {            return (String)super.h.invoke(this, m2, null);        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final int count()    {        try        {            return ((Integer)super.h.invoke(this, m4, null)).intValue();        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final Class annotationType()    {        try        {            return (Class)super.h.invoke(this, m5, null);        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final int hashCode()    {        try        {            return ((Integer)super.h.invoke(this, m0, null)).intValue();        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    private static Method m1;    private static Method m3;    private static Method m2;    private static Method m4;    private static Method m5;    private static Method m0;    static     {        try        {            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {                Class.forName("java.lang.Object")            });            m3 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("name", new Class[0]);            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);            m4 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("count", new Class[0]);            m5 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("annotationType", new Class[0]);            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);        }        catch(NoSuchMethodException nosuchmethodexception)        {            throw new NoSuchMethodError(nosuchmethodexception.getMessage());        }        catch(ClassNotFoundException classnotfoundexception)        {            throw new NoClassDefFoundError(classnotfoundexception.getMessage());        }    }}

小结

至此就解决了第一个疑问了,「所谓的注解其实就是一个实现了Annotation的接口,而我们通过反射获取到的实际上是通过JDK动态代理生成的代理类,这个类实现了我们的注解接口」

AnnotationInvocationHandler

那么问题又来了,具体是如何调用的呢?

$Proxy1的count方法为例

public final int count(){    try    {        return ((Integer)super.h.invoke(this, m4, null)).intValue();    }    catch(Error _ex) { }    catch(Throwable throwable)    {        throw new UndeclaredThrowableException(throwable);    }}

跟进super

public class Proxy implements java.io.Serializable {    protected InvocationHandler h;}

这个InvocationHandler是谁呢?通过在Proxy(InvocationHandler h)方法上打断点追踪结果如下:


原来我们对于count方法的调用传递给了AnnotationInvocationHandler

看看它的invoke逻辑

public Object invoke(Object var1, Method var2, Object[] var3) {    //var4-方法名    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;        switch(var4.hashCode()) {            case -1776922004:                if (var4.equals("toString")) {                    var7 = 0;                }                break;            case 147696667:                if (var4.equals("hashCode")) {                    var7 = 1;                }                break;            case 1444986633:                if (var4.equals("annotationType")) {                    var7 = 2;                }        }        switch(var7) {            case 0:                return this.toStringImpl();            case 1:                return this.hashCodeImpl();            case 2:                return this.type;            default:                //因为我们是count方法,走这个分支                Object var6 = this.memberValues.get(var4);                if (var6 == null) {                    throw new IncompleteAnnotationException(this.type, var4);                } else if (var6 instanceof ExceptionProxy) {                    throw ((ExceptionProxy)var6).generateException();                } else {                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {                        var6 = this.cloneArray(var6);                    }     //返回var6                    return var6;                }        }    }}

这个memberValues是啥?

private final Map<String, Object> memberValues;

他是一个map,存放的是方法名(String)与值的键值对

这里以count()方法的invoke执行为例


可以看到它走了default的分支,从上面的map中取到了,我们所定义的2020,那这个memberValues是什么时候解析出来的呢?

通过查看方法调用栈,我们发现在下图这个时候countname还没有赋值


在方法中加入断点重新调试得到如下结果


2020出现了,再跟进parseMemberValue方法中,再次重新调试


再跟进parseConst方法


康康javap反编译的字节码中的常量池吧

#71 = Integer            2020

好巧啊,正好是2020!!

因此发现最后是从ConstantPool中根据偏移量来获取值的,至此另一个疑问也解决了,我们在注解中设置的方法,最终在调用的时候,是从一个以<方法名,属性值>为键值对的map中获取属性值,定义成方法只是为了在反射调用作为参数而已,所以也可以将它看成属性吧。

总结

运行时注解的产生作用的步骤如下:

  1. 对annotation的反射调用使得动态代理创建实现该注解的一个类
  2. 代理背后真正的处理对象为AnnotationInvocationHandler,这个类内部维护了一个map,这个map的键值对形式为<注解中定义的方法名,对应的属性名>
  3. 任何对annotation的自定义方法的调用(抛开动态代理类继承自object的方法),最终都会实际调用AnnotatiInvocationHandler的invoke方法,并且该invoke方法对于这类方法的处理很简单,拿到传递进来的方法名,然后去查map
  4. map中memeberValues的初始化是在AnnotationParser中完成的,是勤快的,在方法调用前就会初始化好,缓存在map里面
  5. AnnotationParser最终是通过ConstantPool对象从常量池中拿到对应的数据的,再往下ConstantPool对象就不深入了

编译时注解初探

由于编译时注解的很多处理逻辑内化在Javac中,这里不做过多探讨,仅对《深入理解JVM》中的知识点进行梳理和总结。

在JDK5中,Java语言提供了对于注解的支持,此时的注解只在程序运行时发挥作用,但是在JDK6中,JDK新加入了一组插入式注解处理器的标准API,这组API使得我们对于注解的处理可以提前至编译期,从而影响到前端编译器的工作!!常用的Lombok就是通过注解处理器来实现的

「自定义简单注解处理器」

实现自己的注解处理器,首先需要继承抽象类javax.annotation.processing.AbstractProcessor,只有process()方法需要我们实现,process()方法如下:

//返回值表示是否修改Element元素public abstract boolean process(Set<? extends TypeElement> annotations,                                RoundEnvironment roundEnv);
  • annotations:这个注解处理器处理的注解集合
  • roundEnv:当前round的抽象语法树结点,每一个结点都为一个Element,一共有18种Element包含了Java中 的所有元素:
  • PACKAGE(包)
  • ENUM(枚举)
  • CLASS(类)
  • ANNOTATION_TYPE(注解)
  • INTERFACE(接口)
  • ENUM_CONSTANT(枚举常量)
  • FIELD(字段)
  • PARAMETER(参数)
  • LOCAL_VARIABLE(本地变量)
  • EXCEPTION_PARAMETER(异常)
  • METHOD(方法)
  • CONSTRUCTOR(构造方法)
  • STATIC_INIT(静态代码块)
  • INSTANCE_INIT(实例代码块)
  • TYPE_PARAMETER(参数化类型,泛型尖括号中的)
  • RESOURCE_VARIABLE(资源变量,try-resource)
  • MODULE(模块)
  • OTHER(其他)

此外还有一个重要的实例变量processingEnv,它提供了上下文环境,需要创建新的代码,向编译器输出信息,获取其他工具类都可以通过它

实现一个简单的编译器注解处理器也非常简单,继承AbstractProcessor实现process()方法,在process()方法中实现自己的处理逻辑即可,此外需要两个注解配合一下:

  • @SupportedAnnotationTypes:该注解处理器处理什么注解
  • @SupportedSourceVersion:注解处理器支持的语言版本

「实例」

@SupportedAnnotationTypes("com.hustdj.jdkStudy.annotation.PrintMsg")@SupportedSourceVersion(SourceVersion.RELEASE_8)public class PrintNameProcessor extends AbstractProcessor {    @Override    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {        Messager messager = processingEnv.getMessager();        for (Element element : roundEnv.getRootElements()) {            messager.printMessage(Diagnostic.Kind.NOTE,"my name is "+element.toString());        }        //不修改语法树,返回false        return false;    }}

输出如下:

G:\ideaIU\ideaProjects\cookcode\src\main\java>javac com\hustdj\jdkStudy\annotation\PrintMsg.javaG:\ideaIU\ideaProjects\cookcode\src\main\java>javac com\hustdj\jdkStudy\annotation\PrintNameProcessor.javaG:\ideaIU\ideaProjects\cookcode\src\main\java>javac -processor com.hustdj.jdkStudy.annotation.PrintNameProcessor com\hustdj\jdkStudy\annotation\AnnotationTest.java警告: 来自注释处理程序 'com.hustdj.jdkStudy.annotation.PrintNameProcessor' 的受支持 source 版本 'RELEASE_8' 低于 -source '1.9'注: my name is com.hustdj.jdkStudy.annotation.AnnotationTest1 个警告

最后给大家送下福利,大家可以在后台私信面试,即可以获取一份我整理的最新Java面试题资料。

相关推荐

这些端口关闭后,系统会更安全!系统高危端口及其关闭方法?

在这高速发展的网络信息时代,信息安全显得非常重要,病毒、木马、非法侵入等安全事件经常发生。在我们使用电脑过程中,为了确保系统安全,以下高危端口必须关闭,防患于未然。一.Windows系统的445端口...

什么是安全组_什么是安全组件

安全组是一种虚拟防火墙,具备状态检测和数据包过滤功能,用于在云计算环境中设置网络访问控制,保护云服务器(ECS实例)、负载均衡、云数据库等资源。核心特性:虚拟防火墙:安全组控制云资源的出入站流量,决定...

针对单个网站的渗透思路(精)_网站渗透步骤

欢迎搜索公众号:白帽子左一每天分享更多黑客技能,工具及体系化视频教程(免费领首先,当我们拿到一个网站的域名或者IP的时候。最先要做的是信息收集。下面着重介绍一下信息收集模块一、信息收集——端口扫描与分...

风险突出的高危端口汇总 一网打尽 !

高危端口一直是攻击者关注的焦点,了解这些端口的风险、攻击方式及防护策略至关重要。一、文件传输类端口1.TCP20/21:FTP服务端口FTP(文件传输协议)用于文件的上传和下载。其明文传输特性使得...

指定IP地址进行远程访问服务器设置方法(windows系统)

我们有很多服务器经常受到外界网络的干扰,入侵者们通过扫描3389端口爆破密码非法进入我们的服务器,这时,我们可以配置服务器IP安全策略来限制一些IP访问,大大提高了服务器的安全。实验环境:服务端:...

服务器被黑,如何查找入侵、攻击痕迹呢?

本文出自头条号老王谈运维,转载请说明出处。引言:随着网络的越来越普及,使用的越来越频繁,木马病毒也随之侵入进来并且肆无忌惮。如何将病毒拒之门外,已成为我们普通大众必须具备的一项技能。这样,你才能使木马...

win10 telnet命令怎么查看端口是否打开

可能大家也会遇到这个问题,win10telnet命令查看端口是否打开的步骤是什么?具体方法如下:1、键盘输入快捷键WIN+R,打开运行窗口。2、输入cmd,点击确定按钮。3、弹出cmd命令行窗...

Crysis勒索病毒针对政企服务器攻击升级 腾讯安全展开全面防御

近日,腾讯安全御见威胁情报中心监测发现,Crysis勒索病毒在国内传播升级,感染数量呈上升趋势,该病毒主要通过RDP弱口令爆破传播入侵政企机构,加密重要数据,由于该病毒的加密破坏暂无法解密,被攻击后将...

Windows端口详解,这几个端口不能开!

一、血泪警告这7个端口开着电脑秒变公共厕所445端口:勒索病毒专用通道永恒之蓝病毒最爱突破口,文件共享功能成致命漏洞。企业内网还敢用用,个人电脑开着就是作死135-139端口:网络邻居成内鬼,Wind...

网络通讯笔记_网络通讯笔记怎么写

网络通讯一、NIC(网卡)二、CMD命令提示符三、服务、协议与端口常见的计算机服务常见的计算机端口与协议四、DOS命令1、基本DOS命令五、地址一、NIC(网卡)网络接口控制器又叫网络适配器也就是...

服务器远程端口是什么意思?什么是服务器远程端口?

什么是服务器远程端口?如图:IP冒号后面的数字这就是服务器的一个远程端口服务器远程端口是什么意思?服务器远程端口是服务器通信服务中的一个服务端窗口号码,取值范围是1-65535.一个服务器里面包含服务...

服务器节点到底是啥?看完这篇全明白,旧电脑也能派上大用场

不少朋友看了我用旧电脑改服务器节点的文章,后台都在问:“服务器节点到底能干啥?”其实这东西没那么神秘,今天用大白话讲讲,看完你就知道家里的旧设备藏着多大潜力。服务器节点:网络世界的“小工位”简单说...

广东通管局预警:勒索病毒威胁“关键信息基础设施”,应高度警惕

来源:澎湃新闻据广东省通信管理局网站消息,广东省通信管理局5月12日发布了《关于勒索病毒对关键信息基础设施威胁的预警通报》。通报称,5月7日,美国最大燃油运输管道商“科洛尼尔”(ColonialP...

80端口和443端口是什么?服务器端口干什么用的?

80和443端口是最常见的2个端口,都是提供网络WEB浏览服务所需要的端口,一台服务器通过不同的端口,提供不同的服务。80端口服务:HTTP(HyperTextTransportProtocol)...

从单日网络安全风险看当前网络安全状况

一、核心结论(从单日数据看全局风险)通过对2025年8月18日这一天的非法访问数据深度分析,可以清晰看到:网络环境中的安全威胁呈现高频次、多目标、全球化三大特征。单日4557次非法访问尝试,覆盖22、...

取消回复欢迎 发表评论: