尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java八股篇——注解

Java八股篇——注解 一、八股原文小林 Coding 关于注解原理的原文是注解本质是一个继承了 Annotation 的特殊接口其具体实现类是 Java 运行时生成的动态代理类。我们通过反射获取注解时返回的是 Java 运行时生成的动态代理对象。通过代理对象调用自定义注解的方法会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。下面拆成 4 层来看。二、第一层注解是「特殊接口」平时写的普通接口publicinterfaceUserService{voidhello();}注解长得很像但它是一种独立的类型publicinterfaceMyAnnotation{Stringvalue();}interface和interface不是一回事interface → 声明一个「注解类型」 interface → 声明一个普通接口定义好之后就能往类上贴MyAnnotation(hello)publicclassUser{}三、第二层它隐式继承了 Annotation所有注解类型都隐式继承java.lang.annotation.Annotation。你不用自己写extends AnnotationJava 规定好了MyAnnotation ↓ java.lang.annotation.Annotation所以这行会返回trueAnnotation.class.isAssignableFrom(MyAnnotation.class);// true但别把它理解成普通接口那种简单的implements关系。说「所有注解类型都属于 Annotation 体系」是对的再往下就不是普通接口那一套了。四、第三层实现类是运行时生成的动态代理类假设publicinterfaceMyAnnotation{Stringvalue();}MyAnnotation(hello)publicclassUser{}然后去取这个注解运用到了反射的知识MyAnnotationannotationUser.class.getAnnotation(MyAnnotation.class);你可能以为annotation是一个MyAnnotation对象其实它不是你 new 出来的而是 Java 运行时给你的一个代理对象。粗略可以想成// 帮助理解的伪代码JDK 真正生成的代理类不是这么简单写出来的classMyAnnotationProxyimplementsMyAnnotation{OverridepublicStringvalue(){returnhello;}}代理对象它和目标实现同一个接口对外看起来和目标一模一样。有点像秘书和老板的关系办的业务是一模一样的但是秘书帮老板干了所有事情但是秘书代理对象并不是老板真实对象这里补一个原文没提、但很关键的点MyAnnotation必须标Retention(RetentionPolicy.RUNTIME)否则getAnnotation()拿到的直接是null。五、为什么需要代理对象因为MyAnnotation本质上是一个接口而接口不能newMyAnnotation();// 编译报错但你确实需要一个能用的对象。于是 JDK 在运行时生成一个实现了该注解接口的代理对象交给你。所以annotation.value();实际上是在调用这个代理对象的方法。但是实际上这个annotation不是普通对象是JDK 动态代理生成的代理对象。只要调用代理对象上任何接口方法**不会直接执行方法体注解接口本来就没有方法体全部被拦截统一进入AnnotationInvocationHandler.invoke()。这个动作就叫代理对象拦截调用。拦截调用JDK 发现这是代理对象的接口方法不尝试执行不存在的方法体直接把这次方法调用方法名value、参数交给AnnotationInvocationHandler的invoke()六、AnnotationInvocationHandler 是什么这是原文最容易把人绕晕的地方。JDK 内部的链路大概是这样annotation.value() ↓ 代理对象拦截调用 ↓ InvocationHandler ↓ AnnotationInvocationHandler ↓ invoke() ↓ 按方法名找到对应的值 ↓ 返回 hello所以不用把它想得多神秘。它本质上就是帮这个注解代理对象处理方法调用的一个处理器。七、先看一个普通的动态代理例子不用注解先看最普通的动态代理长什么样。publicinterfacePerson{StringgetName();}常规做法是自己写实现类PersonpersonnewPersonImpl();person.getName();person ↓ PersonImpl ↓ getName()动态代理把它换成person ↓ 代理对象运行时生成没有源码 ↓ InvocationHandler ↓ invoke() ↓ 决定返回什么// 1. 定义调用处理器所有接口方法调用都会进到这里InvocationHandlerhandler(proxy,method,args)-{System.out.println(有人调用了method.getName());return张三;};// 2. 创建代理对象Personperson(Person)Proxy.newProxyInstance(Person.class.getClassLoader(),newClass[]{Person.class},handler);System.out.println(person.getName());输出有人调用了getName 张三我一行class PersonImpl implements Person都没写person.getName()照样能调。这就是动态代理。八、注解走的是同一套机制回到MyAnnotation(hello)publicclassUser{}MyAnnotationannotationUser.class.getAnnotation(MyAnnotation.class);拿到的就是运行时生成的代理对象。所以annotation.value()大概走annotation.value() ↓ 动态代理对象 ↓ InvocationHandler ↓ AnnotationInvocationHandler.invoke() ↓ 找到 value ↓ 返回 hello和上一节的Person是同一个套路只不过这次接口是注解类型。九、memberValues 是什么这部分其实很简单。假设注解有多个成员publicinterfaceMyAnnotation{Stringvalue();intage();}MyAnnotation(value张三,age20)publicclassUser{}运行时代理对象内部得记住这两个键值对value → 张三 age → 20这种结构正好用Map装MapString,ObjectmemberValuesnewHashMap();memberValues.put(value,张三);memberValues.put(age,20);于是annotation.value();可以理解成memberValues.get(value)得到张三annotation.age()就是memberValues.get(age)得到20。这就是原文说的AnnotationInvocationHandler的invoke方法会从memberValues这个 Map 里索引出对应的值。十二、回忆用的那张图代码 │ MyAnnotation(hello) ↓ 编译 ↓ User.class │ 保存注解相关元数据 ↓ 程序运行 ↓ getAnnotation() ↓ JDK 解析注解信息 ↓ 生成 / 返回注解代理对象 ↓ MyAnnotation annotation ↓ annotation.value() ↓ 代理对象拦截调用 ↓ AnnotationInvocationHandler ↓ invoke() ↓ memberValues │ value → hello ↓ 返回 hello简单的来说注解原理就是 注解就是写在代码上的元数据运行时注解可以被反射读取JDK 用代理对象表示这个注解调用注解方法时代理机制会从保存的注解信息里把对应的值取出来。Question结合文档内容简单说说注解的底层实现可以按「编译期存下来、运行期造对象、调用时查 Map」三步说第一步注解类型编译后是一个继承Annotation的接口。用interface声明的注解本质是特殊的接口类型隐式继承java.lang.annotation.Annotation所以Annotation.class.isAssignableFrom(MyAnnotation.class)返回true。第二步注解值在编译后进入 class 文件的注解元数据。写的MyAnnotation(hello)会被编译进User.class以注解属性的形式保存。运行时通过反射User.class.getAnnotation(MyAnnotation.class)读取时JDK 解析这些元数据。第三步因为注解是接口、不能直接 newJDK 就用动态代理生成一个实现了该注解接口的代理对象返回。调用annotation.value()时调用先被代理拦截交给sun.reflect.annotation.AnnotationInvocationHandler的invoke()方法处理。JDK 事先把注解的成员名和值放进 handler 内部的一个MapString, Object memberValuesinvoke()直接从里面按方法名取值返回所以annotation.value()等价于memberValues.get(value)。另外注解必须标Retention(RetentionPolicy.RUNTIME)才能在运行时被反射读到不加时默认是CLASS级别编译后就丢了getAnnotation()返回null。如果面试官继续追问 JDK 内部怎么生成代理类、memberValues具体怎么解析那属于ProxyGenerator、AnnotationParser这些源码层面的细节一般一轮面试不会问到这个深度。
返回列表