这一篇 Java 注解,写得太好了(java注解教程)
off999 2024-10-28 16:49 14 浏览 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
到这里就有两个疑问了:
- getAnnotation获取到的是什么?一个实例?注解是一个类?
- 我们明明调用的是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是什么时候解析出来的呢?
通过查看方法调用栈,我们发现在下图这个时候count和name还没有赋值
在方法中加入断点重新调试得到如下结果
2020出现了,再跟进parseMemberValue方法中,再次重新调试
再跟进parseConst方法
康康javap反编译的字节码中的常量池吧
#71 = Integer 2020
好巧啊,正好是2020!!
因此发现最后是从ConstantPool中根据偏移量来获取值的,至此另一个疑问也解决了,我们在注解中设置的方法,最终在调用的时候,是从一个以<方法名,属性值>为键值对的map中获取属性值,定义成方法只是为了在反射调用作为参数而已,所以也可以将它看成属性吧。
总结
运行时注解的产生作用的步骤如下:
- 对annotation的反射调用使得动态代理创建实现该注解的一个类
- 代理背后真正的处理对象为AnnotationInvocationHandler,这个类内部维护了一个map,这个map的键值对形式为<注解中定义的方法名,对应的属性名>
- 任何对annotation的自定义方法的调用(抛开动态代理类继承自object的方法),最终都会实际调用AnnotatiInvocationHandler的invoke方法,并且该invoke方法对于这类方法的处理很简单,拿到传递进来的方法名,然后去查map
- map中memeberValues的初始化是在AnnotationParser中完成的,是勤快的,在方法调用前就会初始化好,缓存在map里面
- 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面试题资料。
相关推荐
- Python 数据分析——利用Pandas进行分组统计
-
话说天下大势,分久必合,合久必分。数据分析也是如此,我们经常要对数据进行分组与聚合,以对不同组的数据进行深入解读。本章将介绍如何利用Pandas中的GroupBy操作函数来完成数据的分组、聚合以及统计...
- python数据分析:介绍pandas库的数据类型Series和DataFrame
-
安装pandaspipinstallpandas-ihttps://mirrors.aliyun.com/pypi/simple/使用pandas直接导入即可importpandasas...
- 使用DataFrame计算两列的总和和最大值_[python]
-
【如果对您有用,请关注并转发,谢谢~~】最近在处理气象类相关数据的空间计算,在做综合性计算的时候,DataFrame针对每列的统计求和、最大值等较为方便,对某行的两列或多列数据进行求和与最大值等的简便...
- 8-Python内置函数
-
Python提供了丰富的内置函数,这些函数可以直接使用而无需导入任何模块。以下是一些常用的内置函数及其示例:1-print()1-1-说明输出指定的信息到控制台。1-2-例子2-len()2-1-说...
- Python中函数式编程函数: reduce()函数
-
Python中的reduce()函数是一个强大的工具,它通过连续地将指定的函数应用于序列(如列表)来对序列(如列表)执行累积操作。它是functools模块的一部分,这意味着您需要在使用它之...
- 万万没想到,除了香农计划,Python3.11竟还有这么多性能提升
-
众所周知,Python3.11版本带来了较大的性能提升,但是,它具体在哪些方面上得到了优化呢?除了著名的“香农计划”外,它还包含哪些与性能相关的优化呢?本文将带你一探究竟!作者:BeshrKay...
- 最全python3.11版12类75个内置函数大全
-
获取全部内置函数:importbuiltins#导入模块yc=[]#异常属性nc=[]#不可调用fn=[]#内置函数defll(ty=builtins):...
- 软件测试笔试题
-
测试工程师岗位,3-5年,10-14k1.我司有一款产品,类似TeamViewer,向日葵,mstsc,QQ远程控制产品,一个PC客户端产品,请设想一下测试要点。并写出2.写出常用的SQL语句8条,l...
- 备战各大互联网巨头公司招聘会,最全Python面试大全,共300题
-
前言众所周知,越是顶尖的互联网公司在面试这一part的要求就越高,需要你有很好的技术功底、项目经验、一份漂亮的简历,当然还有避免不了的笔试过关。对于Python的工程师来说,全面掌握好有关Python...
- 经典 SQL 数据库笔试题及答案整理
-
马上又是金三银四啦,有蛮多小伙伴在跳槽找工作,但对于年限稍短的软件测试工程师,难免会需要进行笔试,而在笔试中,基本都会碰到一道关于数据库的大题,今天这篇文章呢,就收录了下最近学员反馈上来的一些数据库笔...
- 用Python开发日常小软件,让生活与工作更高效!附实例代码
-
引言:Python如何让生活更轻松?在数字化时代,编程早已不是程序员的专属技能。Python凭借其简洁易学的特点,成为普通人提升效率、解决日常问题的得力工具。无论是自动化重复任务、处理数据,还是开发个...
- 太牛了!102个Python实战项目被我扒到了!建议收藏!
-
挖到宝了!整整102个Python实战项目合集,从基础语法到高阶应用全覆盖,附完整源码+数据集,手把手带你从代码小白变身实战大神!这波羊毛不薅真的亏到哭!超全项目库,学练一站式搞定这份资...
- Python中的并发编程
-
1.Python对并发编程的支持多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成。多进程:multiprocessing,利用多核CPU...
- Python 也有内存泄漏?
-
1.背景前段时间接手了一个边缘视觉识别的项目,大功能已经开发的差不多了,主要是需要是优化一些性能问题。其中比较突出的内存泄漏的问题,而且不止一处,有些比较有代表性,可以总结一下。为了更好地可视化内存...
- python爬虫之多线程threading、多进程、协程aiohttp批量下载图片
-
一、单线程常规下载常规单线程执行脚本爬取壁纸图片,只爬取一页的图片。importdatetimeimportreimportrequestsfrombs4importBeautifu...
你 发表评论:
欢迎- 一周热门
-
-
python 3.8调用dll - Could not find module 错误的解决方法
-
加密Python源码方案 PyArmor(python项目源码加密)
-
Python3.8如何安装Numpy(python3.6安装numpy)
-
大学生机械制图搜题软件?7个受欢迎的搜题分享了
-
编写一个自动生成双色球号码的 Python 小脚本
-
免费男女身高在线计算器,身高计算公式
-
将python文件打包成exe程序,复制到每台电脑都可以运行
-
Python学习入门教程,字符串函数扩充详解
-
Python数据分析实战-使用replace方法模糊匹配替换某列的值
-
Python进度条显示方案(python2 进度条)
-
- 最近发表
- 标签列表
-
- python计时 (54)
- python安装路径 (54)
- python类型转换 (75)
- python进度条 (54)
- python的for循环 (56)
- python串口编程 (60)
- python写入txt (51)
- python读取文件夹下所有文件 (59)
- java调用python脚本 (56)
- python操作mysql数据库 (66)
- python字典增加键值对 (53)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python qt (52)
- python人脸识别 (54)
- python斐波那契数列 (51)
- python多态 (60)
- python命令行参数 (53)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- centos7安装python (53)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)