Java註解是如何玩轉的,面試官和我聊了半個小時

怎麼獲取屬性上的註解

Java註解是如何玩轉的,面試官和我聊了半個小時

作者: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方法。

  • 顶部