天天看點

動态代理模式下UndeclaredThrowableException的産生

作者:Java解白

API文檔

我們先來看下這個異常類的api文檔:

Thrown by a method invocation on a proxy instance if its invocation handler's invoke method throws a checked exception (a Throwable that is not assignable to RuntimeException or Error) that is not assignable to any of the exception types declared in the throws clause of the method that was invoked on the proxy instance and dispatched to the invocation handler.

這段描述中介紹了異常會被抛出的情況:調用代理執行個體的增強方法,如果調用處理程式(增強器)的invoke方法中抛出一個檢查異常,但該異常不能被throws子句中聲明的任何異常捕獲(預設是RuntimeException和Error),那麼UndeclaredThrowableException這個異常就會被代理執行個體抛出。

代碼示範

由于是使用JDK的動态代理進行示範,那肯定少不了接口類:

public interface Animal {
    // 奔跑
    void run();
}
複制代碼           

被代理類:

public class Pig implements Animal {
    @Override
    public void run() {
        System.out.println("豬突猛進");
    }
}
複制代碼           

以及增強器InvocationHandler

public class AnimalInvocationHandler implements InvocationHandler {

    private final Object target;

    public AnimalInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("增強方法 -> className: " + target.getClass().getSimpleName() + " methodName:" + method.getName());
        method.invoke(target, args);
        throw new Exception("throw not catch exception");
    }
}
複制代碼           

一切準備就緒,開始測試

public static void main(String[] args) throws Exception {

    // 1、建立 InvocationHandler 執行個體并設定代理的目标類對象
    Animal pig = new Pig();
    InvocationHandler invocationHandler = new AnimalInvocationHandler(pig);

    // 2、建立代理類型,擷取一個帶有InvocationHandler參數的構造器
    Class<?> proxyClass = Proxy.getProxyClass(Animal.class.getClassLoader(), Animal.class);
    Constructor<?> ProxyConstructor = proxyClass.getConstructor(InvocationHandler.class);

    // 3、以構造器的方式建立代理執行個體,執行增強方法
    Animal animalProxy = (Animal) ProxyConstructor.newInstance(invocationHandler);
    try {
        animalProxy.run();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
複制代碼           

啟動main方法後,控制台列印:

增強方法 -> className:Pig  methodName:run
豬突猛進
java.lang.reflect.UndeclaredThrowableException
	at com.sun.proxy.$Proxy0.run(Unknown Source)
	at com.learn.springtest.undeclaredthrowable.MainClass.main(MainClass.java:40)
Caused by: java.lang.Exception: throw not catch exception
	at com.learn.springtest.undeclaredthrowable.AnimalInvocationHandler.invoke(AnimalInvocationHandler.java:22)
	... 2 more
複制代碼           

從上面的結果輸出中,可以明顯看到:代理類抛出的這個異常,而根本原因是AnimalInvocationHandler的invoke方法中抛出了Exception異常。

解釋:由于Exception不在throws子句中聲明的任何異常(Animal#run方法沒有聲明抛出異常,預設就是RuntimeException和Error)的範圍内,異常無法被捕獲。最終,代理類上抛了UndeclaredThrowableException異常,事實也确實如此!

兩個影響因素

從上面的叙述中,可以得到一個結論,那就是影響代理類能否抛出UndeclaredThrowableException的因素有兩個:

  1. 被代理類的方法上聲明的抛出異常;
  2. 增強器InvocationHandler的invoke方法中抛出的異常類型;

接下來,将以實驗的方式驗證這兩個影響因素,畢竟偉大的領袖毛主席曾說過:實踐是檢驗真理的唯一标準。

實驗1:

其他代碼不變,被代理類的方法上抛出Exception異常,改動如下:

void run() throws Exception;
複制代碼           

執行main方法,控制台輸出:

增強方法 -> className:Pig  methodName:run
豬突猛進
java.lang.Exception: throw not catch exception
	at com.learn.springtest.undeclaredthrowable.AnimalInvocationHandler.invoke(AnimalInvocationHandler.java:22)
	at com.sun.proxy.$Proxy0.run(Unknown Source)
	at com.learn.springtest.undeclaredthrowable.MainClass.main(MainClass.java:40)
複制代碼           

實驗2:

其他代碼不變,增強器AnimalInvocationHandler中抛出的異常改為RuntimeException,改動如下:

throw new RuntimeException("throw not catch exception");
複制代碼           

執行main方法,控制台輸出:

增強方法 -> className:Pig  methodName:run
豬突猛進
java.lang.RuntimeException: throw not catch exception
	at com.learn.springtest.undeclaredthrowable.AnimalInvocationHandler.invoke(AnimalInvocationHandler.java:22)
	at com.sun.proxy.$Proxy0.run(Unknown Source)
	at com.learn.springtest.undeclaredthrowable.MainClass.main(MainClass.java:40)
複制代碼           

通過上面實驗中的針對性改動,兩次的運作結果中都沒有再出現UndeclaredThrowableException異常,那麼這兩個影響因素也就得到了證明。

位元組碼檔案

生成代理執行個體的位元組碼檔案

public static void main(String[] args) throws Exception {

    // 測驗代碼,忽略
    ...

    // 儲存代理類
    saveGeneratedJdkProxyFiles();
    saveClass("Pig$Proxy0", proxyClass.getInterfaces(), "");
}

/**
 * 開啟設定:允許生成Java動态代理生成的類檔案
 */
public static void saveGeneratedJdkProxyFiles() throws Exception {
    Field field = System.class.getDeclaredField("props");
    field.setAccessible(true);
    Properties props = (Properties) field.get(null);
    props.put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
}

/**
 * 生成代理類 class 并保持到檔案中
 *
 * @param className  生成的代理類名稱
 * @param interfaces 代理類的實作接口
 * @param pathDir    代理類儲存的目錄路徑,需要以目錄分隔符"/"結尾
 */
public static void saveClass(String className, Class<?>[] interfaces, String pathDir) {
    byte[] classFile = ProxyGenerator.generateProxyClass(className, interfaces);

    // 如果目錄不存在就建立所有子目錄
    Path path = Paths.get(pathDir);
    if (!path.toFile().exists()) {
        path.toFile().mkdirs();
    }

    String fullFilePath = pathDir + className + ".class";
    try (FileOutputStream fos = new FileOutputStream(fullFilePath)) {
        fos.write(classFile);
        fos.flush();
        System.out.println("代理類的class檔案寫入成功");
    } catch (Exception e) {
        e.printStackTrace();
    }
}
複制代碼           

執行main方法,在目前項目的根目錄下可以找到一個名稱為Pig$Proxy0.class的檔案,它就是代理類的位元組碼檔案。

貼一下代理類的位元組碼檔案中run()增強方法:

public final void run() throws  {
    try {
        super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}
複制代碼           

這樣就可以很直覺的看到,如果增強器InvocationHandler#invoke方法中抛出的異常,不能被RuntimeException或者Error捕獲,最終就會抛出UndeclaredThrowableException異常。

其他代碼不變,被代理類的方法上抛出IOException異常,執行main方法重新生成位元組碼檔案

public final void run() throws IOException {
    try {
        super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | IOException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}
複制代碼           

增強方法的throws子句中多了一個IOException類型的異常。

Tips

1、saveClass的第三個參數中傳入的是空字元串,這樣生成的代理類位元組碼檔案就會出現在項目的根目錄下,直接使用編輯器(如:IDEA)打開即可。

如果生成在其他地方,就需要使用java位元組碼檔案的反編譯工具,推薦使用 Java Decompiler

下載下傳後解壓,打開程式,直接把.class檔案丢進去就行了。

2、開啟生成位元組檔案的配置方法中,丢進去一個key值為“sun.misc.ProxyGenerator.saveGeneratedFiles”的配置項

如果無法生成位元組碼檔案,那就将配置項的key值改為你目前JDK版本中對應的key值即可