深入分析Java反射(六)-反射調用異常處理


前提

Java反射的API在JavaSE1.7的時候已經基本完善,但是本文編寫的時候使用的是Oracle JDK11,因為JDK11對於sun包下的源碼也上傳了,可以直接通過IDE查看對應的源碼和進行Debug。

本文主要介紹一個使用反射一定會遇到的問題-反射調用異常處理。

反射調用異常處理

反射調用出現異常的方法主要考慮下面的情況:

  • 屬性操作:java.lang.reflect.Field#set(Object obj, Object value)java.lang.reflect.Field#get(Object obj)
  • 構造器調用:java.lang.reflect.Constructor#newInstance(Object ... initargs)
  • 方法調用:java.lang.reflect.Method#invoke(Object obj, Object... args)

處理屬性操作異常

先看設置屬性的方法:

public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException

實際上,通過方法注釋可以得知會拋出四種異常:

  • IllegalAccessException:非法訪問異常,注意它是檢查(checked)異常,也就是需要顯示捕獲,此異常會在修飾符禁用訪問的時候拋出,可以通過setAccessible(true)抑制修飾符檢查來避免拋出此異常。
  • IllegalArgumentException:非法參數異常,它是運行時異常,當入參實例obj不是當前Field所在類(包括父類、子類和接口)的時候會拋出此異常。
  • NullPointerException:空指針異常,當入參實例obj為null的時候會拋出此異常。
  • ExceptionInInitializerError:初始化器調用異常導致的錯誤,如果由於set(Object obj, Object value)方法引發的初始化失敗會包裝成ExceptionInInitializerError,此異常的父類為Error,常見的發生情況就是靜態成員或者靜態代碼塊依賴到反射屬性設置。

前面三種異常都很好理解,最后一個ExceptionInInitializerError可能有點陌生,它的拋出條件是:在靜態代碼塊初始化解析過程總拋出異常或者靜態變量初始化的時候拋出異常。筆者嘗試了很多例子都沒辦法造出案例,從Stackoverflow找到一個例子:

public class Example {
    public static void main(String[] args) throws Exception {
        Field field = Fail.class.getDeclaredField("number");
        field.set(null, 42); // Fail class isn't initialized at this point
    }
}

class Fail {
    static int number;
    static {
        boolean val = true;
        if (val)
            throw new RuntimeException(); // causes initialization to end with an exception
    }
}

簡單來說就是:靜態代碼塊和靜態變量的初始化順序和它們在類文件編寫的順序是一致的,如果一個類未初始化直接使用它的靜態代碼塊和靜態變量通過Field#set(Object obj, Object value)調用就會出現ExceptionInInitializerError異常。

屬性的獲取方法拋出的異常和設置值方法是一致的,這里不做詳細展開:

public Object get(Object obj) throws IllegalArgumentException, IllegalAccessException

處理構造器調用異常

構造器調用主要是用於對象的實例化,先看newInstance方法的簽名:

public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
  • InstantiationException:實例化異常,拋出此異常的一般情況是:當前構造所在類型為一個抽象類型。
  • IllegalAccessException:非法訪問異常。
  • IllegalArgumentException:非法參數異常,下面的情況會拋出此異常:參數數量或者類型不匹配,參數列表為原始類型但是實際使用了包裝類型、參數列表為原始類型但是實際使用了包裝類型、構造所在的類是枚舉類型等。
  • InvocationTargetException:目標調用異常,這個是需要處理的重點異常,在下一節"處理方法調用異常"詳細探討。

這里只舉個例子說明一下InstantiationException出現的場景:

public abstract class AbstractSample {

	public AbstractSample() {
	}

	public static void main(String[] args) throws Exception{
		Constructor<AbstractSample> declaredConstructor = AbstractSample.class.getDeclaredConstructor();
		declaredConstructor.newInstance();
	}
}

像上面的抽象類AbstractSample包含一個默認的公有構造,使用Constructor#newInstance()會拋出InstantiationException異常:

Exception in thread "main" java.lang.InstantiationException
	at java.base/jdk.internal.reflect.InstantiationExceptionConstructorAccessorImpl.newInstance(InstantiationExceptionConstructorAccessorImpl.java:48)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
	at club.throwable.jdk.sample.reflection.reflect.AbstractSample.main(AbstractSample.java:18)

處理方法調用異常

方法調用是反射中使用頻率最高的反射操作,主要是Method#invoke(Object obj, Object... args)方法:

public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

主要包括以下幾種異常:

  • IllegalAccessException:非法訪問異常。
  • IllegalArgumentException:非法參數異常,下面的情況會拋出此異常:入參obj並不是當前實例方法對應的實例對象、參數數量或者類型不匹配,參數列表為原始類型但是實際使用了包裝類型、參數列表為原始類型但是實際使用了包裝類型等等。
  • NullPointerException:空指針異常,入參obj為null時候會拋出此異常。
  • ExceptionInInitializerError:初始化器調用異常導致的錯誤。
  • InvocationTargetException:目標調用異常。

重點看InvocationTargetException(繼承自ReflectiveOperationException,而ReflectiveOperationException繼承自Exception,也就是它是checked異常,必須顯式捕獲):

public class InvocationTargetException extends ReflectiveOperationException {

    private static final long serialVersionUID = 4085088731926701167L;
    
    // 持有的目標異常實例
    private Throwable target;

    public InvocationTargetException(Throwable target) {
        super((Throwable)null);  // Disallow initCause
        this.target = target;
    }

    public InvocationTargetException(Throwable target) {
        super((Throwable)null);  // Disallow initCause
        this.target = target;
    } 

    public Throwable getTargetException() {
        return target;
    }

    public Throwable getCause() {
        return target;
    }    
}    

從注釋中得知:方法(Method)或者構造(Constructor)調用異常會拋出此InvocationTargetException異常,用於包裝源異常,源異常實例作為目標被InvocationTargetException通過成員target持有,可以通過InvocationTargetException#getTargetException()或者InvocationTargetException#getCause()獲取原始的目標異常。這里注意到,InvocationTargetException在覆蓋父類構造的時候使用了null,所以調用其getMessage()方法會得到null。

舉個例子:

public class InvocationTargetExceptionMain {

	public void method() {
		throw new NullPointerException("Null");
	}

	public static void main(String[] args) throws NoSuchMethodException, SecurityException {
		InvocationTargetExceptionMain main = new InvocationTargetExceptionMain();
		Method method = InvocationTargetExceptionMain.class.getDeclaredMethod("method");
		try {
			method.invoke(main);
		} catch (IllegalAccessException e) {
			//no-op
		} catch (InvocationTargetException e) {
			System.out.println("InvocationTargetException#message:" + e.getMessage());
			if (e.getTargetException() instanceof NullPointerException) {
				NullPointerException nullPointerException = (NullPointerException) e.getTargetException();
				System.out.println("NullPointerException#message:" + nullPointerException.getMessage());
			}
		}
	}
}

運行后輸出:

InvocationTargetException#message:null
NullPointerException#message:Null

構造器Constructor#newInstance()中拋出InvocationTargetException的場景是類似的。

小結

在反射操作中,方法調用的頻次是最高的,其次是通過構造器實例化對象。需要重點關注這兩個地方的異常處理,特別是異常類型InvocationTargetException,緊記需要獲取原始目標異常類型再進行判斷,否則很容易導致邏輯錯誤(最近筆者在做一個功能的時候剛好踩了這個坑)。

個人博客

(本文完 e-a-20181215 c-2-d)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM