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

Posted javatiange

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java注解是如何玩转的,字节跳动面试官和我聊了半个小时相关的知识,希望对你有一定的参考价值。

面试官:自定义的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";  

所以,这里生成的代理类类名格式为:包名+ P r o x y + n u m , 如 j d k p r o x y . Proxy+num,如jdkproxy. Proxy+numjdkproxy.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方法。

一直想整理出一份完美的面试宝典,但是时间上一直腾不开,这套一千多道面试题宝典,结合今年金三银四各种大厂面试题,以及 GitHub 上 star 数超 30K+ 的文档整理出来的,我上传以后,毫无意外的短短半个小时点赞量就达到了 13k,说实话还是有点不可思议的。

一千道互联网 Java 工程师面试题

内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、mysql、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)

初级—中级—高级三个级别的大厂面试真题

阿里云——Java 实习生/初级

List 和 Set 的区别 HashSet 是如何保证不重复的

HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?

HashMap 的扩容过程

HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

对象的四种引用

Java 获取反射的三种方法

Java 反射机制

Arrays.sort 和 Collections.sort 实现原理 和区别

Cloneable 接口实现原理

异常分类以及处理机制

wait 和 sleep 的区别

数组在内存中如何分配

答案展示:

美团——Java 中级

BeanFactory 和 ApplicationContext 有什么区别

Spring Bean 的生命周期

Spring IOC 如何实现

说说 Spring AOP

Spring AOP 实现原理

动态代理(cglib 与 JDK)

Spring 事务实现方式

Spring 事务底层原理

如何自定义注解实现功能

Spring MVC 运行流程

Spring MVC 启动流程

Spring 的单例实现原理

Spring 框架中用到了哪些设计模式

为什么选择 Netty

说说业务中,Netty 的使用场景

原生的 NIO 在 JDK 1.7 版本存在 epoll bug

什么是 TCP 粘包/拆包

TCP 粘包/拆包的解决办法

Netty 线程模型

说说 Netty 的零拷贝

Netty 内部执行流程

答案展示:

蚂蚁金服——Java 高级

题 1:

  1. jdk1.7 到 jdk1.8 Map 发生了什么变化(底层)?

  2. ConcurrentHashMap

  3. 并行跟并发有什么区别?

  4. jdk1.7 到 jdk1.8 java 虚拟机发生了什么变化?

  5. 如果叫你自己设计一个中间件,你会如何设计?

  6. 什么是中间件?

  7. ThreadLock 用过没有,说说它的作用?

  8. Hashcode()和 equals()和==区别?

  9. mysql 数据库中,什么情况下设置了索引但无法使用?

  10. mysql 优化会不会,mycat 分库,垂直分库,水平分库?

  11. 分布式事务解决方案?

  12. sql 语句优化会不会,说出你知道的?

  13. mysql 的存储引擎了解过没有?

  14. 红黑树原理?

题 2:

  1. 说说三种分布式锁?

  2. redis 的实现原理?

  3. redis 数据结构,使⽤场景?

  4. redis 集群有哪⼏种?

  5. codis 原理?

  6. 是否熟悉⾦融业务?记账业务?蚂蚁⾦服对这部分有要求。

好啦~展示完毕,大概估摸一下自己是青铜还是王者呢?

前段时间,在和群友聊天时,把今年他们见到的一些不同类别的面试题整理了一番,于是有了以下面试题集,也一起分享给大家~

如果你觉得这些内容对你有帮助,可以加入csdn进阶交流群,领取资料

基础篇


JVM 篇


MySQL 篇



Redis 篇




由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

需要的小伙伴,可以一键三连,下方获取免费领取方式!
在这里插入图片描述

以上是关于Java注解是如何玩转的,字节跳动面试官和我聊了半个小时的主要内容,如果未能解决你的问题,请参考以下文章

记一次C++后台开发面试拷打过程

搞硬件的同事,最近和我聊了一件事

分享给各位道友一份2021年征战蚂蚁金服头条拼多多的面试总结经验包

字节跳动+阿里+华为+腾讯等大厂Java面试题,你不懂还不学?

作为字节跳动面试官,java获取当前时间戳打印

Java面试送分题:java函数式编程举例