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


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

面試官:自定義的Java註解是如何生效的?

小白:自定義註解後,需要定義這個註解的註解解析及處理器,在這個註解解析及處理器的內部,通過反射使用Class、Method、Field對象的getAnnotation()方法可以獲取各自位置上的註解信息,進而完成註解所需要的行為,例如給屬性賦值、查找依賴的對象實例等。

面試官:你說的是運行時的自定義註解解析處理,如果要自定義一個編譯期生效的註解,如何實現?

小白:自定義註解的生命週期在編譯期的,聲明這個註解時@Retention的值為RetentionPolicy.CLASS,需要明確的是此時註解信息保留在源文件和字節碼文件中,在JVM加載class文件後,註解信息不會存在內存中。聲明一個類,這個類繼承javax.annotation.processing.AbstractProcessor抽象類,然後重寫這個抽象類的process方法,在這個方法中完成註解所需要的行為。

面試官:你剛剛說的這種方式的實現原理是什麼?

小白:在使用javac編譯源代碼的時候,編譯器會自動查找所有繼承自AbstractProcessor的類,然後調用它們的process方法,通過RoundEnvironment#getElementsAnnotatedWith方法可以獲取所有標註某註解的元素,進而執行相關的行為動作。

面試官:有如下的一個自定義註解,在使用這個註解的時候,它的value值是存在哪的?

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

小白:使用javap -verbose命令查看這個註解的class文件,發現這個註解被編譯成了接口,並且繼承了java.lang.annotation.Annotation接口,接口是不能直接實例化使用的,當在代碼中使用這個註解,並使用getAnnotation方法獲取註解信息時,JVM通過動態代理的方式生成一個實現了Test接口的代理對象實例,然後對該實例的屬性賦值,value值就存在這個代理對象實例中。

<code>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)/<code>

面試官:有沒有看過這部分的實現源代碼?

小白:看過,如果順著getAnnotation方法繼續跟蹤源代碼,會發現創建代理對象是在AnnotationParser.java中實現的,這個類中有一個annotationForMap方法,它的具體代碼如下:

<code>    public static Annotation annotationForMap(
Class type, Map<string>memberValues) {
return (Annotation) Proxy.newProxyInstance(
type.getClassLoader(), newClass[] { type },

new AnnotationInvocationHandler(type, memberValues));
}/<string>/<code>

這裡使用Proxy.newProxyInstance方法在運行時動態創建代理,AnnotationInvocationHandler實現了InvocationHandler接口,當調用代理對象的value()方法獲取註解的value值,就會進入AnnotationInvocationHandler類中的invoke方法,深入invoke方法會發現,獲取value值最終是從AnnotationInvocationHandler類的memberValues屬性中獲取的,memberValues是一個Map類型,key是註解的屬性名,這裡就是“value”,value是使用註解時設置的值。

<code>    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;
}
}
}
}/<code>

面試官:JDK動態代理創建中的InvocationHandler充當什麼樣的角色?

小白:InvocationHandler是一個接口,代理類的調用處理器,每個代理對象都具有一個關聯的調用處理器,用於指定動態生成的代理類需要完成的具體操作。該接口中有一個invoke方法,代理對象調用任何目標接口的方法時都會調用這個invoke方法,在這個方法中進行目標類的目標方法的調用。

面試官:對於JDK動態代理,生成的代理類是什麼樣的?為什麼調用代理類的任何方法時都一定會調用invoke方法?

小白:假設有一個LoginService接口,這個接口中只有一個login方法,LoginServiceImpl實現了LoginService接口,同時使用Proxy.newProxyInstance創建代理,具體代碼如下:

<code>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();
}
}
}/<code>

這個要從Proxy.newProxyInstance方法的源碼開始分析,這個方法用於創建代理類對象,具體代碼段如下:

<code>        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());
}/<object>/<code>

上面的代碼段中,先關注一下如下代碼:

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

用於獲取代理類的構造函數,constructorParams參數其實就是一個InvocationHandler,所以從這裡猜測代理類中有一個InvocationHandler類型的屬性,並且作為構造函數的參數。那這個代理類是在哪裡創建的?注意看上面的代碼段中有:

<code>Class> cl = getProxyClass0(loader, intfs);/<code>

這裡就是動態創建代理類的地方,繼續深入到getProxyClass0方法中,方法如下:

<code>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);
}/<code>

繼續跟蹤代碼,進入proxyClassCache.get(loader, interfaces),這個方法中重點關注如下代碼:

<code>Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));/<code>

繼續跟蹤代碼,進入subKeyFactory.apply(key, parameter),進入apply方法,這個方法中有很多重要的信息,如生成的代理類所在的包名,發現重要代碼:

<code>long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;/<code>

上面代碼用於生成代理類名稱,nextUniqueNumber是AtomicLong類型,是一個全局變量,所以nextUniqueNumber.getAndIncrement()會使用當前的值加一得到新值;proxyClassNamePrefix聲明如下:

<code>private static final String proxyClassNamePrefix = "$Proxy";/<code>

所以,這裡生成的代理類類名格式為:包名+$Proxy+num,如jdkproxy.$Proxy12。

代理類的類名已經構造完成了,那可以開始創建代理類了,繼續看代碼,

<code>byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);/<code>

這裡就是真正創建代理類的地方,繼續分析代碼,進入generateProxyClass方法,

<code>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;
}/<code>

從這裡可以很直白的看到,生成的代理類字節碼文件被輸出到某個目錄下了,這裡可能很難找到這個字節碼文件,沒關係,仔細查看這個方法,generateProxyClass方法可以重用,可以在外面調用generateProxyClass方法,把生成的字節碼文件輸出到指定位置。寫到這裡,終於可以解釋上面實例代碼中的createProxyClassFile方法了,這個方法把代理類的字節碼文件輸出到了/Users路徑下,直接到路徑下查看LoginServiceProxy文件,使用反編譯工具查看,得到的代碼如下,

<code>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());
}
}
}/<code>

從上面的代碼可以看到,當代理類調用目標方法時,會調用InvocationHandler接口實現類的invoke方法,很明瞭的解釋了為什麼調用目標方法時一定會調用invoke方法。


分享到:


相關文章: