注解动态赋值_Java注解是如何玩转的,面试官和我聊了半个小时

32a30a8e63408163fbe52f34c7cd9873.png

作者:wind瑞 来自:JavaQ

面试官:自定义的Java注解是如何生效的?

小白:自定义注解后,需要定义这个注解的注解解析及处理器,在这个注解解析及处理器的内部,通过反射使用Class、Method、Field对象的getAnnotation()方法可以获取各自位置上的注解信息,进而完成注解所需要的行为,例如给属性赋值、查找依赖的对象实例等。

面试官:你说的是运行时的自定义注解解析处理,如果要自定义一个编译期生效的注解,如何实现?

小白:自定义注解的生命周期在编译期的,声明这个注解时@Retention的值为RetentionPolicy.CLASS,需要明确的是此时注解信息保留在源文件和字节码文件中,在JVM加载class文件后,注解信息不会存在内存中。声明一个类,这个类继承javax.annotation.processing.AbstractProcessor抽象类,然后重写这个抽象类的process方法,在这个方法中完成注解所需要的行为。

面试官:你刚刚说的这种方式的实现原理是什么?

小白:在使用javac编译源代码的时候,编译器会自动查找所有继承自AbstractProcessor的类,然后调用它们的process方法,通过RoundEnvironment#getElementsAnnotatedWith方法可以获取所有标注某注解的元素,进而执行相关的行为动作。

面试官:有如下的一个自定义注解,在使用这个注解的时候,它的value值是存在哪的?

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface Test {     String value() default "";}

小白:使用javap -verbose命令查看这个注解的class文件,发现这个注解被编译成了接口,并且继承了java.lang.annotation.Annotation接口,接口是不能直接实例化使用的,当在代码中使用这个注解,并使用getAnnotation方法获取注解信息时,JVM通过动态代理的方式生成一个实现了Test接口的代理对象实例,然后对该实例的属性赋值,value值就存在这个代理对象实例中。

Classfile /Test/bin/Test.class    Last modified 2020-3-23; size 423 bytes  MD5 checksum be9fb08ef7e5f2c4a1bca7d6f856cfa5  Compiled from "Test.java"public interface Test extends java.lang.annotation.Annotation  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATIONConstant pool:   #1 = Class              #2             // Test   #2 = Utf8               Test   #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               value   #8 = Utf8               ()Ljava/lang/String;   #9 = Utf8               AnnotationDefault  #10 = Utf8               T  #11 = Utf8               SourceFile  #12 = Utf8               Test.java  #13 = Utf8               RuntimeVisibleAnnotations  #14 = Utf8               Ljava/lang/annotation/Target;  #15 = Utf8               Ljava/lang/annotation/ElementType;  #16 = Utf8               TYPE  #17 = Utf8               Ljava/lang/annotation/Retention;  #18 = Utf8               Ljava/lang/annotation/RetentionPolicy;  #19 = Utf8               RUNTIME{  public abstract java.lang.String value();    descriptor: ()Ljava/lang/String;    flags: ACC_PUBLIC, ACC_ABSTRACT    AnnotationDefault:      default_value: s#10}SourceFile: "Test.java"RuntimeVisibleAnnotations:  0: #14(#7=[e#15.#16])  1: #17(#7=e#18.#19)

面试官:有没有看过这部分的实现源代码?

小白:看过,如果顺着getAnnotation方法继续跟踪源代码,会发现创建代理对象是在AnnotationParser.java中实现的,这个类中有一个annotationForMap方法,它的具体代码如下:

    public static Annotation annotationForMap(        Class type, MapmemberValues) {        return (Annotation) Proxy.newProxyInstance(            type.getClassLoader(), newClass[] { type },            new AnnotationInvocationHandler(type, memberValues));    }

这里使用Proxy.newProxyInstance方法在运行时动态创建代理,AnnotationInvocationHandler实现了InvocationHandler接口,当调用代理对象的value()方法获取注解的value值,就会进入AnnotationInvocationHandler类中的invoke方法,深入invoke方法会发现,获取value值最终是从AnnotationInvocationHandler类的memberValues属性中获取的,memberValues是一个Map类型,key是注解的属性名,这里就是“value”,value是使用注解时设置的值。

    public Object invoke(Object var1, Method var2, Object[] var3) {        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:                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);                    }                    return var6;                }            }        }    }

面试官:JDK动态代理创建中的InvocationHandler充当什么样的角色?

小白:InvocationHandler是一个接口,代理类的调用处理器,每个代理对象都具有一个关联的调用处理器,用于指定动态生成的代理类需要完成的具体操作。该接口中有一个invoke方法,代理对象调用任何目标接口的方法时都会调用这个invoke方法,在这个方法中进行目标类的目标方法的调用。

面试官:对于JDK动态代理,生成的代理类是什么样的?为什么调用代理类的任何方法时都一定会调用invoke方法?

小白:假设有一个LoginService接口,这个接口中只有一个login方法,LoginServiceImpl实现了LoginService接口,同时使用Proxy.newProxyInstance创建代理,具体代码如下:

public interface LoginService {    voidlogin();}public class LoginServiceImpl implements LoginService {    @Override    public void login() {        System.out.println("login");    }}public class ProxyInvocationHandler implements InvocationHandler {    private LoginService loginService;    public ProxyInvocationHandler (LoginService loginService) {        this.loginService = loginService;    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        beforeLogin();        Object invokeResult = method.invoke(loginService, args);        afterLogin();        return invokeResult;    }    private void beforeLogin() {        System.out.println("before login");    }    private void afterLogin() {        System.out.println("after login");    }}public classClient{    @Test    public voidt est() {        LoginService loginService = new LoginServiceImpl();        ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler(loginService);        LoginService loginServiceProxy = (LoginService) Proxy.newProxyInstance(loginService.getClass().getClassLoader(), loginService.getClass().getInterfaces(), proxyInvocationHandler);        loginServiceProxy.login();        createProxyClassFile();    }    public static void createProxyClassFile() {        String name = "LoginServiceProxy";        byte[] data = ProxyGenerator.generateProxyClass(name, new Class[]{LoginService.class});        try {            FileOutputStream out = new FileOutputStream("/Users/" + name + ".class");            out.write(data);            out.close();        } catch (Exception e) {            e.printStackTrace();        }    }}

这个要从Proxy.newProxyInstance方法的源码开始分析,这个方法用于创建代理类对象,具体代码段如下:

        Class> cl = getProxyClass0(loader, intfs);        /*         * Invoke its constructor with the designated invocation handler.         */        try {            final Constructor> cons = cl.getConstructor(constructorParams);            final InvocationHandler ih = h;            if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {                // create proxy instance with doPrivilege as the proxy class may                // implement non-public interfaces that requires a special permission                return AccessController.doPrivileged(new PrivilegedAction() {                    public Object run() {                        return newInstance(cons, ih);                    }                });            } else {                return newInstance(cons, ih);            }        } catch (NoSuchMethodException e) {            throw new InternalError(e.toString());        }

上面的代码段中,先关注一下如下代码:

final Constructor> cons = cl.getConstructor(constructorParams);

用于获取代理类的构造函数,constructorParams参数其实就是一个InvocationHandler,所以从这里猜测代理类中有一个InvocationHandler类型的属性,并且作为构造函数的参数。那这个代理类是在哪里创建的?注意看上面的代码段中有:

Class> cl = getProxyClass0(loader, intfs);

这里就是动态创建代理类的地方,继续深入到getProxyClass0方法中,方法如下:

private static Class> getProxyClass0(ClassLoader loader,                                           Class>... interfaces) {        if (interfaces.length > 65535) {            throw new IllegalArgumentException("interface limit exceeded");        }        // If the proxy class defined by the given loader implementing        // the given interfaces exists, this will simply return the cached copy;        // otherwise, it will create the proxy class via the ProxyClassFactory        return proxyClassCache.get(loader, interfaces);    }

继续跟踪代码,进入proxyClassCache.get(loader, interfaces),这个方法中重点关注如下代码:

Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));

继续跟踪代码,进入subKeyFactory.apply(key, parameter),进入apply方法,这个方法中有很多重要的信息,如生成的代理类所在的包名,发现重要代码:

long num = nextUniqueNumber.getAndIncrement();String proxyName = proxyPkg + proxyClassNamePrefix + num;

上面代码用于生成代理类名称,nextUniqueNumber是AtomicLong类型,是一个全局变量,所以nextUniqueNumber.getAndIncrement()会使用当前的值加一得到新值;proxyClassNamePrefix声明如下:

private static final String proxyClassNamePrefix = "$Proxy";

所以,这里生成的代理类类名格式为:包名+$Proxy+num,如jdkproxy.$Proxy12。

代理类的类名已经构造完成了,那可以开始创建代理类了,继续看代码,

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);

这里就是真正创建代理类的地方,继续分析代码,进入generateProxyClass方法,

public static byte[] generateProxyClass(final String var0, Class[] var1) {        ProxyGenerator var2 = new ProxyGenerator(var0, var1);        final byte[] var3 = var2.generateClassFile();        if(saveGeneratedFiles) {            AccessController.doPrivileged(new PrivilegedAction() {                public Void run() {                    try {                        FileOutputStream var1 = new FileOutputStream(ProxyGenerator.dotToSlash(var0) + ".class");                        var1.write(var3);                        var1.close();                        return null;                    } catch (IOException var2) {                        throw new InternalError("I/O exception saving generated file: " + var2);                    }                }            });        }        return var3;    }

从这里可以很直白的看到,生成的代理类字节码文件被输出到某个目录下了,这里可能很难找到这个字节码文件,没关系,仔细查看这个方法,generateProxyClass方法可以重用,可以在外面调用generateProxyClass方法,把生成的字节码文件输出到指定位置。写到这里,终于可以解释上面实例代码中的createProxyClassFile方法了,这个方法把代理类的字节码文件输出到了/Users路径下,直接到路径下查看LoginServiceProxy文件,使用反编译工具查看,得到的代码如下,

public final class LoginServiceProxy extends Proxy  implements LoginService{  private static Method m1;  private static Method m3;  private static Method m0;  private static Method m2;  public LoginServiceProxy(InvocationHandler paramInvocationHandler)    throws   {    super(paramInvocationHandler);  }  public final boolean equals(Object paramObject)    throws   {    try    {      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();    }    catch (Error|RuntimeException localError)    {      throw localError;    }    catch (Throwable localThrowable)    {      throw new UndeclaredThrowableException(localThrowable);    }  }  public final void login()    throws   {    try    {      this.h.invoke(this, m3, null);      return;    }    catch (Error|RuntimeException localError)    {      throw localError;    }    catch (Throwable localThrowable)    {      throw new UndeclaredThrowableException(localThrowable);    }  }  public final int hashCode()    throws   {    try    {      return ((Integer)this.h.invoke(this, m0, null)).intValue();    }    catch (Error|RuntimeException localError)    {      throw localError;    }    catch (Throwable localThrowable)    {      throw new UndeclaredThrowableException(localThrowable);    }  }  public final String toString()    throws   {    try    {      return (String)this.h.invoke(this, m2, null);    }    catch (Error|RuntimeException localError)    {      throw localError;    }    catch (Throwable localThrowable)    {      throw new UndeclaredThrowableException(localThrowable);    }  }  static  {    try    {      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });      m3 = Class.forName("jdkproxy.LoginService").getMethod("login", new Class[0]);      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);      return;    }    catch (NoSuchMethodException localNoSuchMethodException)    {      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());    }    catch (ClassNotFoundException localClassNotFoundException)    {      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());    }  }}

从上面的代码可以看到,当代理类调用目标方法时,会调用InvocationHandler接口实现类的invoke方法,很明了的解释了为什么调用目标方法时一定会调用invoke方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/332873.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里云服务器如何创建快照备份数据

文章目录创建快照快照收费你申请了阿里云服务器,其实就是申请了一个云盘,这个云盘安装了操作系统,你可以远程访问这个系统并使用该系统。我们可以给云盘创建快照,从而达到备份服务器数据的目的。创建快照 快照收费 如果您计划不再…

shebang_Java 11:运行单文件程序和“ shebang”脚本

shebang在Java 11中,对java启动器进行了增强,可以直接运行单文件源代码程序,而不必先编译它们。 例如,考虑以下仅添加其参数的类: import java.util.*; public class Add {public static void main(String[] args) {…

iShot快捷键

快捷键说明Option A选择截图区域Option Z截图光标下的窗口Option X截图整个屏幕Option D延时截图Option W开始录屏/结束录屏

日期格式化为yyyymmdd_Excel小技巧——如何将多行日期快速转换为数字文本

Hello~大家好,本来计划这一期给大家介绍一下如何根据身份证号快速获得性别信息的小技巧,但是在上一节内容如何将数字快速转换成日期格式发布后,就有朋友私信问我,可不可以再介绍一下将日期快速转换成数字文本的方法呢?…

顽皮狗 多线程分享_谁去过顽皮,谁去过尼斯? 圣诞老人为您提供Java 11建议!...

顽皮狗 多线程分享有没有想过圣诞老人如何为世界各地的孩子们提供节日礼物? 有20亿个孩子,每个孩子都有自己的愿望清单,他会在24小时内完成。 这意味着平均每个孩子43微秒,他需要检查每个孩子是否顽皮或好。 您无需再怀疑了。 我…

2021新交规超速处罚规定

1.时速超过限定时速不到10%的,给予警告; 2.在限速为50公里以下的道路,时速超过限定时速10%以上不到20%的(必须低于时速55公里),处50元罚款;超过限定时速20%以上不到50%的,处100元罚…

处理接口超时_架构设计|异步请求如何同步处理?

本文创意来自一次业务需求,这次需要接入一个第三方外部服务。由于这个服务只提供异步 API,为了不影响现有系统同步处理的方式,接入该外部服务时,应用对外屏蔽这种差异,内部实现异步请求同步。全文摘要:异步…

使用Spring Boot和MongoDB构建一个React式应用程序

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。 如果您要处理大量流数据,React式应用程序可让您更好地扩展。 它们是非阻塞…

jax-ws和jax-rs_JAX-RS和OpenAPI对Hypermedia API的支持:任重而道远

jax-ws和jax-rs或早或晚,大多数积极使用REST(ful) Web服务和API的开发人员偶然发现了这种真正的外星事物,即HATEOAS : 超文本作为应用程序状态的引擎 。 对HATEOAS是什么及其与REST的关系的好奇最终将导致发现Richards…

如何下载MySQL的JDBC驱动包

1.打开 MySQL 官网:https://www.mysql.com/ 2.点击 DOWNLOADS,把页面滚动到最下面,点击 MySQL Community (GPL) Downloads 3.点击 Connector/J 4.点击 General Availability(GA) Releases,在 Select Operating System 下拉列表选…

IntelliJ IDEA for Mac如何添加jar包/如何导入jar包/导包

文章目录如何将 jar 包添加到已存在的资源库中将 jar 包添加到项目级别的资源库中将 jar 包添加到全局级别的资源库中将 jar 包添加到模块级别的资源库中创建资源库创建模块级别的资源库创建项目级别的资源库创建全局级别的资源库给某个模块单独添加 jar 包模块的依赖标签页&am…

r2dbc_使用Spring Data R2DBC进行异步RDBMS访问

r2dbc不久之前,发布了JDBC驱动程序的React性变体。 称为R2DBC。 它允许将数据异步流传输到已预订的任何端点。 通过将R2DBC之类的React性驱动程序与Spring WebFlux结合使用,可以编写一个完整的应用程序,以异步方式处理数据的接收和发送。 在本…

python中布尔类型是特殊的_Python中的特殊方法以及应用详解

前言Python 中的特殊方法主要是为了被解释器调用的,因此应该尽量使用 len(my_object) 而不是 my_object.__len__() 这种写法。在执行 len(my_object) 时,Python 解释器会自行调用 my_object 中实现的 __len__ 方法。除非有大量的元编程存在,直…

IntelliJ IDEA 自动补全变量名称和变量类型(自动补全变量的声明内容)

文章目录varOption EnterOption Command Vvar // 如下声明定义一个字符串变量 String s new String(); // 1 先编写 new String() // 2 在 new String() 后面输入 .var 直接回车,即可得到我们想要的变量了 // 3 上面生成的变量名可能不是我们想要的&#xff0c…

jdk11 jdk12_JDK 12附带紧凑数字格式

jdk11 jdk12JDK 12 Early Access Build 24引入了对紧凑数字格式的支持 。 JDK-8188147(紧凑数字格式支持)CSR的“摘要”是简单的句子,“添加了对JDK中的紧凑/短数字格式的支持。” 同一CSR还提供了详细的“解决方案”部分,该部分提…

全连接层 时间复杂度_神经网络全连接层(3)

CNN网络基础结构神经网络-全连接层(3)上一回我们聊完了算法,这回我们正式开始写代码。上回在做公式推导的时候,我们实际上只是针对一个数据样本进行推导,而实际中,计算和训练都是一批一批完成的。大多数机器学习训练都有batch的概…

line和spline_探索适用于Apache Spark的Spline Data Tracker和可视化工具(第1部分)

line和spline最近引起我注意的一个有趣且很有希望的开源项目是Spline ,它是由Absa维护的Apache Spark数据沿袭跟踪和可视化工具。 该项目由两部分组成:一个在驱动程序上工作的Scala库,该库通过分析Spark执行计划来捕获数据沿袭,以…

MacBook如何快速显示桌面

1.触控板中张开拇指和其它三指 2.通过触发角来快速显示桌面

怎么把word里面虚线变成实线_弱电不会制作cad图,花3分钟看完,只要会用WORD保证你能画出来...

今天我要给你介绍的就是Microsoft Office Visio是Microsoft Office 套件之一。安装Visio之后,可以类比Word的操作方法一样来使用,不过,就是比在Word里画图、修改更方便,功能更强大。特别是在做技术路线图、各种图表的绘图&#xf…

MacBook如何设置分屏浏览的快捷键

MacBook的系统自身无法设置,必须安装第三方软件才能设置,例如:BetterAndBetter、Magnet、BetterSnapTool 等。 BetterAndBetter 的设置,如下图所示: