java异常使用

1.异常的输出流程

  • 第一行:输出 错误类型 + 错误内容
  • 2-N行:输出 错误堆栈 信息

1.常见异常例子:

package com.ztd.question.err;

/**
 * java 异常打印流程
 * @author ztd
 */
public class Demo01SimpleError {
    public static void main(String[] args) {
        new Demo01SimpleError().m1();
    }
    public void m1() {
        m2();
    }
    public void m2() {
        try {
            throw new Exception("出问题了");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

java打印的结果如下:

java.lang.Exception: 出问题了
	at com.ztd.question.err.Demo01SimpleError.m2(Demo01SimpleError.java:16)
	at com.ztd.question.err.Demo01SimpleError.m1(Demo01SimpleError.java:12)
	at com.ztd.question.err.Demo01SimpleError.main(Demo01SimpleError.java:9)

2.异常为什么会这么输出?

这就需要查看一下 e.printStackTrace() 这个方法的实现了,这个方法是Throwable类里面的一个方法,这个方法在控制台里面输出的内容之所以是红色,是因为它使用的是System.err 这个 PrintStream 输出的,我们通常输出内容使用的是 System.out 对象输出的,编辑器可以根据 输出对象 的不同来在控制台展示不同的颜色

真正执行的方法如下:

private void printStackTrace(PrintStreamOrWriter s) {
        Set<Throwable> dejaVu =
            Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
        dejaVu.add(this);

        synchronized (s.lock()) {
            // 1.打印的第一条错误信息
            s.println(this);
            StackTraceElement[] trace = getOurStackTrace();
            // 2.打印堆栈信息
            for (StackTraceElement traceElement : trace)
                s.println("\tat " + traceElement);
					
            // 3.打印被压制的错误
            for (Throwable se : getSuppressed())
                se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
            // 4.打印导致错误的原因
            Throwable ourCause = getCause();
            if (ourCause != null)
                ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
        }
    }

1.输出错误类型 + 消息内容

s.println(this) 方法输出的就是 toString()方法的信息,这个方法也是用的 Throwable 类本身自己的 toString() 方法,可以看到这个方法内容如下:就是输出当前类的 名称 + : + 错误信息

 public String toString() {
        String s = getClass().getName();
        String message = getLocalizedMessage();
        return (message != null) ? (s + ": " + message) : s;
    }

对比一下上面的错误信息完全一致

java.lang.Exception: 出问题了

2.输出错误堆栈信息

之后打印的是堆栈的调用链信息,堆栈的调用关系是从下往上调用的,因为堆栈调用方式本身就是后进先出,所以报错的位置一定在堆栈的最上方,堆栈的初始调用位置一定在最下方。这个堆栈的形成是在 new Exception 的时候就初始化好的。

在执行 new Exception() 的时候,可以看到方法的执行如下:

public Exception(String message) {
		super(message);
}

super 方法执行的是 Throwable 类里面的方法

public Throwable(String message) {
    fillInStackTrace();
    detailMessage = message;
}
public synchronized Throwable fillInStackTrace() {
  if (stackTrace != null ||
      backtrace != null /* Out of protocol state */ ) {
    fillInStackTrace(0);
    stackTrace = UNASSIGNED_STACK;
  }
  return this;
}
// 最终执行了 本地方法 来生成堆栈信息
private native Throwable fillInStackTrace(int dummy);

其中 fillInStackTrace() 方法最终调用本地方法生成堆栈信息数组 StackTraceElement[]。这个 StackTraceElement类里面包含了当前方法执行所在的 类名方法名文件名代码行号

public final class StackTraceElement implements java.io.Serializable {
    // Normally initialized by VM (public constructor added in 1.5)
    private String declaringClass;
    private String methodName;
    private String fileName;
    private int    lineNumber;
}

这样在输出错误堆栈的时候,就可以拼接出来每一条堆栈信息了

打印堆栈的代码如下:at…

for (StackTraceElement traceElement : trace)
  // 堆栈打印代码 at ...
  s.println("\tat " + traceElement);

这个时候,我们看到堆栈打印的直接是对象,所以看一下StackTraceElement的toString方法:

    public String toString() {
        return getClassName() + "." + methodName +
            (isNativeMethod() ? "(Native Method)" :
             (fileName != null && lineNumber >= 0 ?
              "(" + fileName + ":" + lineNumber + ")" :
              (fileName != null ?  "("+fileName+")" : "(Unknown Source)")));
    }

可以看到,这就是我们看到的错误消息:用到了 类名称、方法名称、文件名称、代码行号

at com.ztd.question.err.SimpleError.m2(SimpleError.java:16)

如果这个方法是本地方法,还会带上 (Native Method) 标识,如果是本地方法就不会显示类文件名称和行号了,因为不存在

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

3.被压制的异常:suppressed exceptions

1.异常什么情况下会被隐藏

看下面的这种情况,本来在catch之后需要抛出异常的,但是因为finally里面也抛出了异常,那么这个时候 catch 抛出的异常就被隐藏了

package com.ztd.question.err;

/**
 * 被压制的异常
 * @author ztd
 */
public class Demo02SuppressException {
    public static void main(String[] args) {
        try{
            process("");
        }catch(Exception e){
            System.out.println("catched:");
            throw new RuntimeException(e);
        }finally{
            System.out.println("finally");
            throw new NullPointerException();
        }
    }
    static void process(String s){
        throw new IllegalArgumentException();
    }
}

打印结果如下:

catched:
finally
Exception in thread "main" java.lang.NullPointerException
	at com.ztd.question.err.Demo02SuppressException.main(Demo02SuppressException.java:16)

2.如何打印隐藏的异常

这个时候如果想要把2个异常都打印出来,该怎么办?Throwable里面提供了一个 addSuppressed() 方法,用于把隐藏的异常也打印出来

package com.ztd.question.err;

/**
 * 被隐藏的异常
 * @author ztd
 */
public class Demo03PrintSuppressException {
    public static void main(String[] args) throws Exception {
        Exception origin = null;
        try{
            process("");
        }catch(Exception e){
            origin =e;
            throw new RuntimeException(e);
        }finally{
            try{
                throw new NullPointerException();
            }catch(Exception e){
                if (origin!=null){
                    origin.addSuppressed(e);
                }else{
                    origin=e;
                }
            }
            if (origin!=null){
                throw origin;
            }
        }
    }
    static void process(String s){
        throw new IllegalArgumentException();
    }
}

在打印异常的代码中也可以看到对这个地方进行了处理,而且会加上Suppress标识

// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
  se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);

打印结果如下:

Exception in thread "main" java.lang.IllegalArgumentException
	at com.ztd.question.err.Demo03PrintSuppressException.process(Demo03PrintSuppressException.java:31)
	at com.ztd.question.err.Demo03PrintSuppressException.main(Demo03PrintSuppressException.java:11)
	Suppressed: java.lang.NullPointerException
		at com.ztd.question.err.Demo03PrintSuppressException.main(Demo03PrintSuppressException.java:17)

4.如果异常没有被主动输出会怎么处理?

看下面的例子,虽然报错,但是没有主动调用 e.printStackTrace()

package com.ztd.question.err;

/**
 * 没有主动输出异常
 * @author ztd
 */
public class Demo04ErrorNotPrint {
    public static void main(String[] args) throws Exception {
        throw new Exception("没有主动print的错误");
    }
}

可是结果依然打印了错误日志和调用的堆栈信息

Exception in thread "main" java.lang.Exception: 没有主动print的错误
	at com.ztd.question.err.Demo04ErrorNotPrint.main(Demo04ErrorNotPrint.java:9)

实际上,当一个错误没有地方进行catch 进行处理的时候,jvm会自动调用 Thread 类里面的dispatchUncaughtException如下方法输出异常信息,这就是为什么我们没有手动的输出错误信息,但是信息依然会被打印的原因

   private void dispatchUncaughtException(Throwable e) {
       getUncaughtExceptionHandler().uncaughtException(this, e);
   }

这个方法会获取一个错误处理器,如果不存在就会使用ThreadGroup作为处理器,然后调用如下方法

 public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

输出结果如下:最终还是会像调用 e.printStackTrace() 的效果一样打印错误日志

Exception in thread "main" java.lang.RuntimeException: 运行异常
	at com.ztd.question.proxy.ExceptionTest.main(ExceptionTest.java:17)

5.异常转换

如果一个方法捕获了某个异常后,又在catch语句中抛出新的异常,就相当于把抛出的异常【转换】了。但是这样原有的异常信息不就丢失了吗?我们可以在新的异常的构造方法将原来的异常作为参数,就可以使新的异常保持原有异常的信息了。

package com.ztd.question.err;

/**
 * 异常转换例子
 * @author ztd
 */
public class Demo05ExceptionChange {
    public static void main(String[] args) throws Exception {
        new Demo05ExceptionChange().m2();
    }

    public void m1() throws Exception {
        throw new Exception("m1() 异常");
    }

    public void m2() throws Exception {
        try {
            m1();
        } catch (Exception e) {
            throw new Exception("m2() 异常", e);
        }
    }
}

打印结果如下:

Exception in thread "main" java.lang.Exception: m2() 异常
	at com.ztd.question.err.Demo05ExceptionChange.m2(Demo05ExceptionChange.java:20)
	at com.ztd.question.err.Demo05ExceptionChange.main(Demo05ExceptionChange.java:9)
Caused by: java.lang.Exception: m1() 异常
	at com.ztd.question.err.Demo05ExceptionChange.m1(Demo05ExceptionChange.java:13)
	at com.ztd.question.err.Demo05ExceptionChange.m2(Demo05ExceptionChange.java:18)
	... 1 more

6.动态代理导致的 UndeclaredThrowableException 异常

在RPC接口调用场景或者使用动态代理的场景中,偶尔会出现UndeclaredThrowableException,又或者在使用反射的场景中,出现InvocationTargetException,这都与我们所期望的异常不一致,且将真实的异常信息隐藏在更深一层的堆栈中。本文将重点分析下UndeclaredThrowableException

先给结论

使用jdk动态代理接口时,若方法执行过程中抛出了受检异常但方法签名又没有声明该异常时则会被代理类包装成UndeclaredThrowableException抛出。

问题还原:

// 接口定义
public interface IService {
 void foo() throws SQLException;
}
public class ServiceImpl implements IService{
 @Override
 public void foo() throws SQLException {
  throw new SQLException("I test throw an checked Exception");
 }
}
// 动态代理
public class IServiceProxy implements InvocationHandler {
 private Object target;
 
 IServiceProxy(Object target){
  this.target = target;
 }
 
 @Override
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  return method.invoke(target, args);
 }
}
 
public class MainTest {
 public static void main(String[] args) {
  IService service = new ServiceImpl();
  IService serviceProxy = (IService) Proxy.newProxyInstance(service.getClass().getClassLoader(),
    service.getClass().getInterfaces(), new IServiceProxy(service));
  try {
   serviceProxy.foo();
  } catch (Exception e){
   e.printStackTrace();
  }
 }
}

上面运行得到的异常是:

java.lang.reflect.UndeclaredThrowableException
 at com.sun.proxy.$Proxy0.foo(Unknown Source)
 at com.learn.reflect.MainTest.main(MainTest.java:16)
Caused by: java.lang.reflect.InvocationTargetException
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at com.learn.reflect.IServiceProxy.invoke(IServiceProxy.java:19)
 ... 2 more
Caused by: java.sql.SQLException: I test throw an checked Exception
 at com.learn.reflect.ServiceImpl.foo(ServiceImpl.java:11)
 ... 7 more

而我们的期望是:

java.sql.SQLException: I test throw an checked Exception
 at com.learn.reflect.ServiceImpl.foo(ServiceImpl.java:11)
 ...

原因分析

在上述问题还原中,真实的SQLException被包装了两层,先被 InvocationTargetException 包装,再被UndeclaredThrowableException 包装。 其中,InvocationTargetException 为受检异常,UndeclaredThrowableException 为运行时异常。 为何会被包装呢,还要从动态代理的生成的代理类说起。

jdk动态代理会在运行时生成委托接口的具体实现类,我们通过ProxyGenerator手动生成下class文件,再利用idea解析class文件得到具体代理类 截取部分:

public final class IServiceProxy$1 extends Proxy implements IService {
 private static Method m1;
 private static Method m2;
 private static Method m3;
 private static Method m0;
 
 public IServiceProxy$1(InvocationHandler var1) throws {
  super(var1);
 }
  
 public final void foo() throws SQLException {
  try {
   super.h.invoke(this, m3, (Object[])null);
    // 一下几种异常类型会被抛出,否则会被包装称为 UndeclaredThrowableException 异常
  } catch (RuntimeException | SQLException | Error var2) {
   throw var2;
  } catch (Throwable var3) {
   throw new UndeclaredThrowableException(var3);
  }
 }
 static {
  try {
   m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
   m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
   m3 = Class.forName("com.learn.reflect.IService").getMethod("foo", new Class[0]);
   m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
  } catch (NoSuchMethodException var2) {
   throw new NoSuchMethodError(var2.getMessage());
  } catch (ClassNotFoundException var3) {
   throw new NoClassDefFoundError(var3.getMessage());
  }
 }
}

在调用“委托类”的foo方法时,实际上调用的代理类IServiceProxy$1的foo方法,而代理类主要逻辑是调用InvocationHandler的invoke 方法。 异常处理的逻辑是,对 RuntimeException、接口已声明的异常、Error直接抛出,其他异常被包装成UndeclaredThrowableException抛出。 到这里,或许你已经get了,或许你有疑问,在接口实现中的确是throw new SQLException,为什么还会被包装呢? 再来看 IServiceProxy 的 invoke 方法,它就是直接通过反射执行目标方法,问题就在这里了。 Method.invoke(Object obj, Object… args)方法声明中已解释到,若目标方法抛出了异常,会被包装成 InvocationTargetException。(具体可查看javadoc)

 public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

所以,串起来总结就是: 具体方法实现中 抛出 SQLException 被反射包装为会被包装成 InvocationTargetException,这是个受检异常,而代理类在处理异常时发现该异常在接口中没有声明,所以包装为 UndeclaredThrowableException

解决方法

在实现InvocationHandler的invoke方法体中,对method.invoke(target, args);调用进行try catch,重新 throw InvocationTargetExceptioncause。即:

@Override
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
   return method.invoke(target, args);
  } catch (InvocationTargetException e){
   throw e.getCause();
  }
 }

为什么代理类中对未声明的受检异常转为 UndeclaredThrowableException? 因为Java继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内(意思就是父类的异常要包括子类抛出的异常,否则根据父类引用指向子类实现的时候,如果我们处理父类的异常,却无法覆盖子类的异常,就会出现抛出错误却无法处理的情况:比如父类异常是SQLException,而子类方法却抛出了Exception。那么如果调用端,由于只捕获了SQLException,对于子类抛出了Exception无能为力,这就有问题了,所以java要求子类的异常只能更小)。 代理类实现了父接口或覆盖父类方法,它抛出的异常都在父类的范围内。

 public final void foo() throws SQLException {
  try {
   super.h.invoke(this, m3, (Object[])null);
    // RuntimeException 和 UndeclaredThrowableException 都是非受检异常,可以随意抛出
    // SQLException 个接口声明异常一样,Error 无序程序自己捕获
  } catch (RuntimeException | SQLException | Error var2) {
   throw var2;
  } catch (Throwable var3) {
   throw new UndeclaredThrowableException(var3);
  }
 }

6.自定义异常

为什么要自定义异常?

因为Java 本身自带的异常可能在很多位置都会抛出,而对于我们本身的业务而言,如果我们也抛出Exception或者RuntimeException的话,这些异常将很难被直接定位。如果我们自己定义一些异常,在需要的时候抛出这些异常,那么当我们看到这些错误的时候,就可以很快定位出异常的位置(至少我们可以知道异常使我们业务代码抛出的)而且我们可以根据给异常分配不同的code来区分它的不同含义。

如何自定义异常?

自定义受检异常:继承 Exception

package com.ztd.question.err;

/**
 * 自定义异常
 * @author ztd
 */
public class Demo06MyException {
    public static void main(String[] args) throws MyException {
//        throw new MyException("自定义异常");
//        throw new MyException("自定义异常", new NullPointerException("空指针异常"));
//        throw new MyException(new NullPointerException("空指针异常"));
        throw new MyException("自定义异常", new NullPointerException("空指针异常"), true, true);
    }
}

class MyException extends Exception {
    public MyException() {
    }

    public MyException(String message) {
        super(message);
    }

    public MyException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyException(Throwable cause) {
        super(cause);
    }

    public MyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

自定义非受检异常

package com.ztd.question.err;

public class Demo07MyException {
    public static void main(String[] args) {
        throw new MyRunTimeException("自定义非受检异常");
    }
}

class MyRunTimeException extends RuntimeException {
    public MyRunTimeException() {
    }

    public MyRunTimeException(String message) {
        super(message);
    }

    public MyRunTimeException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyRunTimeException(Throwable cause) {
        super(cause);
    }

    public MyRunTimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}