Java 注解是如何玩转的,面试官和我聊了半个小时

栏目: IT技术 · 发布时间: 4年前

面试官 :自定义的 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_ANNOTATION
Constant 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, Map<String, Object>memberValues) {
        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<Object>() {
                    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方法。

推荐阅读:

String引发的提问,我差点跪了

就写了一行代码,被问了这么多问题

面试官:JVM对锁进行了优化,都优化了啥?

synchronized连环问

高并发编程-ExecutorCompletionService深入解析

如何去除代码中的多次if而引发的一连串面试问题

点点底部右下角" 在看 "呗

Java 注解是如何玩转的,面试官和我聊了半个小时


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

研究之美

研究之美

[美] Donald E. Knuth / 高博 / 电子工业出版社 / 2012-1-1 / 49.00元

《研究之美》是计算机科学大师、“算法分析之父”高德纳(Donald E.Knuth)在20世纪70年代旅居挪威时撰写的适用于计算机科学的一种全新基础数学结构的情景小品。全书以一对追求自由精神生活的青年男女为主人公,展开了一段对于该种全新结构的发现和构造的对白。在此过程中,本书充分展示了计算机科学的从业人员进行全新领域探索时所必备的怀疑、立论、构造、证明、归纳、演绎等逻辑推理和深入反思的能力。《研究......一起来看看 《研究之美》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具