Java异常体系、UncaughtExceptionHandler、Spring MVC统一异常处理、Spring Boot统一异常处理

概述

在这里插入图片描述
所有异常都是继承自java.lang.Throwable类,Throwable有两个直接子类,Error和Exception。

Error用来表示程序底层或硬件有关的错误,这种错误和程序本身无关,如常见的NoClassDefFoundError。这种异常和程序本身无关,不需要检查,属于非受检异常。

Exception表示程序异常,可能是由于程序不严谨导致的,如NPE空指针异常。Exception下面派生RuntimeException和其他异常,其中RuntimeException表示运行时异常,也属于非受检异常。在编译时可以不需要强制检查的异常,不需要显式捕捉或抛出。

除Error和RuntimeException及派生类以外,其他异常都属于受检异常,如IOException、SQLException。在编译时强制进行检查的异常,这种异常需要显式的通过try/catch来捕捉,或通过throws抛出去,否则程序无法通过编译。设计强制检查的异常(受检异常),主要原因是考虑到程序的正确性、稳定性和可靠性。
在这里插入图片描述

try...catch...finally语句块

初中级笔试题可能会出现的知识点。这里直接给出一些结论:

  • 受检异常,需要使用try来包裹可能会抛出异常的代码块,catch用于捕获异常并处理异常的代码块,常见的处理策略包括:打印错误日志、抛出自定义业务异常、释放资源、设置局部变量等
  • 受检异常,还可以直接在方法签名上throws Exception,抛给方法调用者来处理。业务开发中,通常在Service层抛出自定义业务异常,然后在Controller层统一捕获异常并返回errCode和errMsg
  • 不管有没有出现异常,finally仍然会执行
  • 当try和catch中有return时,finally仍然会执行
  • finally常用于释放IO资源、(分布式)锁的持有、

常见异常

初中级Java开发工程师面试中,经常会遇到的一个问题:说说你工作中经常遇到的异常?

面试官指的应该包括Exception和Error,回答问题时,不能只列举Exception。

简单列举Exception如下:

  • NullPointerException:简称NPE。多少人栽在NPE上,多少资金损失是因为NPE。减少(无法杜绝)NPE的方法就是不停地空判断,或使用Optional类。可喜的是,升级到JDK 14以上版本,发生NPE时,JVM会打印具体哪个方法抛的空指针异常,避免同一行代码多个函数调用时无法判断具体是哪个函数抛异常的困扰,方便异常排查;
  • ConcurrentModificationException:简称CME。当有多个迭代器同时遍历和修改Java集合(如ArrayList或HashMap),就有可能抛出CME异常。避免出现CME异常的措施如:加锁,使用CopyOnWriteArrayList,ConcurrentHashMap等集合。
  • IndexOutOfBoundsException:索引越界,实现类有两个ArrayIndexOutOfBoundsException和StringIndexOutOfBoundsException。
  • ClassCastException:类型转换失败。
  • ClassNotFoundException:参考Java学习之NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError

简单列举Error如下:

  • OutOfMemoryError:OOM,报错信息为:java.lang.OutOfMemoryError:Java heap spacess。遇到OOM时,需要先分清楚是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
  • StackOverflowError:栈溢出。栈溢出的原因:递归调用(如求解斐波那契数列问题时),大量循环或死循环,全局变量过多,数组、List、Map数据过大。
  • NoClassDefFoundError:找不到类定义
  • NoSuchMethodError:找不到方法
  • NoSuchFieldError:找不到字段,上面这三种一般都是三方依赖冲突,通过使用maven工具来排查,如mvn dependency:tree > tmp.txt,或使用IDEA的Maven Helper插件

最佳实践

即所谓的Best Practice:

  • 在finally中清理资源;
  • 坚决要杜绝捕获异常后不做任何处理,即catch语句块为空;
  • 捕获异常后的日志打印规范,如记录错误类和方法,记录详细的错误堆栈stacktrace方便排查问题;
  • 使用Try-With-Resource语句,实现AutoCloseable接口的资源;
  • 优先捕获特定的异常,其次再考虑其父类异常;
  • 多使用自定义业务异常,一个异常对应有一个errCode和一个可读性良好的errMsg

进阶

异常表

在JVM中,异常处理不是由字节码指令(早期使用jsr、ret指令)来实现的,而是异常表。

如果一个方法定义有try-catch或try-finally,则会创建异常表,保存异常处理信息:

  • 起始位置
  • 结束位置
  • 程序计数器记录的代码处理的偏移地址
  • 被捕获的异常类在常量池中的索引

Exception table:

Exception table:
    from    to  target type
        0    12    15   Class java/lang/Exception

根据不同的type对应到不同的target上。在操作系统里,这个target也称为异常处理程序。就是特定问题出现时,去异常表查询这个问题对应的是哪个处理程序,然后去执行这个程序,完成异常处理。

面试可能会遇到的问题:finally为什么一定会执行?
查看编译后的字节码,可发现编译器把finally语句块里面的代码分别复制到try和catch语句块里面。

异常throw事件

jvmti中提供两个异常的事件,一个是包含throw和catch,一个是catch。选择功能多的那个方便一点。

void JNICALL Exception(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jmethodID method, jlocation location, jobject exception, jmethodID catch_method, jlocation catch_location)

通过方法签名,可以知道异常的线程,出异常的方法,行号,异常对象,catch的方法和行号。这里由于是触发的throw事件,所以如果只是new Exception的操作是不会触发事件的。有些代码通过创建Exception或Error来控制逻辑,只要不是throw,catch的这种逻辑,这里是检测不到的。如果异常只throw没有catch的话,catch的字段就是空的。

拓展

UncaughtExceptionHandler

在虚拟机中,当一个线程没有显式处理(即try catch)异常而抛出时,会将该异常事件报告给该线程对象的java.lang.Thread.UncaughtExceptionHandler进行处理,如果线程没有设置UncaughtExceptionHandler,则默认会把异常栈信息输出到终端而使程序直接崩溃。所以如果想在线程意外崩溃时做一些处理就可以通过实现UncaughtExceptionHandler来满足需求。

public class Thread {
	/**
	 * 当一个线程因未捕获的异常而即将终止时虚拟机将使用 Thread.getUncaughtExceptionHandler()
	 * 获取已经设置的 UncaughtExceptionHandler 实例,并通过调用其 uncaughtException(...) 方法而传递相关异常信息。
	 * 如果一个线程没有明确设置其 UncaughtExceptionHandler,则将其 ThreadGroup 对象作为其handler,如果 ThreadGroup 对象对异常没有什么特殊的要求,则 ThreadGroup 会将调用转发给默认的未捕获异常处理器(即 Thread 类中定义的静态未捕获异常处理器对象)。
	 */
	@FunctionalInterface
	public interface UncaughtExceptionHandler {
		/**
		 * 未捕获异常崩溃时回调此方法
		 */
		void uncaughtException(Thread t, Throwable e);
	}
	/**
	 * 静态方法,用于设置一个默认的全局异常处理器
	 */
	public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(new RuntimePermission("setDefaultUncaughtExceptionHandler"));
		}
		defaultUncaughtExceptionHandler = eh;
	}
    /**
     * 针对某个Thread对象的方法,用于对特定的线程进行未捕获的异常处理
     */
	public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        checkAccess();
		uncaughtExceptionHandler = eh;
    }
    /**
     * 当Thread崩溃时会调用该方法获取当前线程的 handler,获取不到就会调用 group(handler 类型)。
     * group是Thread类的ThreadGroup类型属性,在Thread构造中实例化
     */
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        if (isTerminated()) {
            // uncaughtExceptionHandler may be set to null after thread terminates
            return null;
        } else {
            UncaughtExceptionHandler ueh = uncaughtExceptionHandler;
            return (ueh != null) ? ueh : getThreadGroup();
        }
    }
    /**
     * 线程全局默认handler
     */
    public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() {
        return defaultUncaughtExceptionHandler;
    }
}

线程崩溃时异常抛出的顺序:

  • 先调用Thread.getUncaughtExceptionHandler()查看是否有自己对象特有的handler,如果有就直接处理
  • 如果没有就调用ThreadGroup(UncaughtExceptionHandler的默认实现类)
  • 如果ThreadGroup没啥特殊处理就会继续调用Thread.getDefaultUncaughtExceptionHandler()获取handler进行处理
  • 如果默认handler也没有处理就直接执行正常的异常流程使程序崩溃。

ThreadGroup核心实现源码:

// ThreadGroup在Thread对象构造方法中实例化
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        // parent默认是null
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            // 一般走进来,调用Thread.setDefaultUncaughtExceptionHandler(...)方法设置全局 handler进行处理
            Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) {
                // 全局handler也不存在就输出异常栈
                System.err.print("Exception in thread \"" + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
}

Spring MVC异常处理机制

参考Spring MVC系列之九大核心组件中的HandlerExceptionResolver部分。

Spring MVC全局异常处理

每个Controller层里的方法都需要进行异常捕获及处理,显然太繁琐且效率低。

自定义类并实现HandlerExceptionResolver接口并重写resolveException方法进行全局异常处理:

@Slf4j
@Component
public class SimpleExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, @NonNull HttpServletResponse response, Object object, @NonNull Exception e) {
        // 业务异常对前端可见,否则统一归为系统异常
        Map<String, Object> map = new HashMap<>();
        map.put("success", false);
        // 自定义业务异常,可多次if判断对应多个异常类型,当然也可使用switch语句
        if (e instanceof BusinessException) {
            map.put("errorMsg", e.getMessage());
        } else {
            map.put("errorMsg", "system exception");
        }
        log.error(e.getMessage(), e);
        // 此处返回ModelandView对象,如error.jsp页面,也可考虑使用其他的模板引擎,如FreeMarker,Thymeleaf
        return new ModelAndView("/error", map);
    }
}

可以以不同的方式将异常结果返回给调用者(前端或其他后端服务)

  • 返回ModelAndView
  • 返回页面的地址
  • 返回JSON
  • 返回HTTP错误码

当然也可以使用下面Spring Boot全局异常处理方案。

Spring Boot全局异常处理

直接给出配置类:

@Slf4j
// 复合注解 = @ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalExceptionHandler {
	// 别的方法都处理不了的异常
    @ExceptionHandler(Exception.class)
    public Response<Object> otherExceptionHandler(HttpServletResponse response, Exception ex) {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        log.error(ex.getMessage(), ex);
        return Response.error("服务器内部异常!");
    }

	// 可捕获自定义异常、JDK或Spring异常,支持数组形式捕获多个不同类型的异常,但推荐一种异常对应一个方法
    @ExceptionHandler({ForbiddenException.class}) // 自定义业务异常
	// @ExceptionHandler({IllegalArgumentException.class}) // JDK异常
	// @ExceptionHandler(HttpMessageNotReadableException.class) // Spring异常
	// 返回Response Status Code
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public Response<Object> forbidden(ForbiddenException e) {
    	// 记录错误日志
        log.error(e.getMessage(), e);
        return Response.error(e.getMessage());
    }

	// 前端(或接口攻击者)使用非法的@RequestBody请求接口,解析异常字段,并将错误日志降级
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Response<Object> validationBodyException(MethodArgumentNotValidException exception) {
        BindingResult result = exception.getBindingResult();
        StringBuilder errorMsg = new StringBuilder();
        if (result.hasErrors()) {
            List<ObjectError> errors = result.getAllErrors();
            errors.forEach(p -> {
                FieldError fieldError = (FieldError) p;
                errorMsg.append(fieldError.getDefaultMessage()).append("!");
                // 设置warn而不是error,日志错误降级
                log.warn("Data check failure : object{" + fieldError.getObjectName() + "},field{" + fieldError.getField() + "},errorMessage{" + fieldError.getDefaultMessage() + "}");
            });
        }
        return Response.error(errorMsg.toString());
    }
}

Response是自定义的数据统一返回格式:

@Data
@NoArgsConstructor
public class Response<T> implements Serializable {
    private int code;
    private String msg;
    private T data;
    // 省略其他包装方法 
}

Dubbo处理异常

分布式调用链

参考

posted @ 2024-08-20 10:04  johnny233  阅读(53)  评论(0编辑  收藏  举报