天天看点

Spring Boot 进阶-Spring Boot的全局异常处理机制详解

作者:架构师面试宝典
Spring Boot 进阶-Spring Boot的全局异常处理机制详解

我们知道在软件运行的过程中,总会出现各种各样的问题,各种各样的异常,而程序员的主要任务之一就是解决在程序运行过程中出现的这些异常。在很多程序员开发的代码中我们会看到在关键的地方为了保证程序能够有一个正常的反馈,大量地使用了try catch finally语句。

大量的try catch语句不但增加了代码的冗余,而且还降低了代码的可读性。所以在Spring Boot 中就有人想将这些try catch的异常处理通过全局的方式进行处理,也就是说在我们正常的开发中就不需要再去关心这些异常的处理了。我们只需要在一个地方进行统一的配置就可以完成全局异常的处理了。

全局异常处理介绍

其实在Spring 3.x的版本中就已经有全局异常处理的注解被提出使用了。例如@ControllerAdvice、@ExceptionHandler、@InitBinder、@ModelAttribute 等等注解。

而这些注解从字面上来看与异常有关的就只有一个@ExceptionHandler 注解,也就是异常处理器的关键注解。其实在Spring中的异常处理机制有两种,一种是全局异常处理、一种是局部异常处理。那么什么是全局异常处理?什么是局部异常处理?

首先局部异常处理顾名思义就是在某些方法或者某些类上对该类中存在的异常进行处理,主要使用的注解就是@ExceptionHandler和@Controller两个注解。这里需要注意的是只有指定了@Controller的类才会被@ExceptionHandler注解所捕获到对应的异常,在实际开发过程中,Controller是不可控的,所以说,这个方式显然不适合在大批量的Controller中使用。那么这就需要用到我们的全局异常处理机制了

全局异常处理机制,首先来讲它可以完成的异常处理在数量上远比局部异常处理要多,其次就是它需要建立一种与局部异常处理机制不同的异常处理全局捕获的方式。所以就出现了@ControllerAdvice注解,,这个注解搭配上@ExceptionHandler 就可以彻底解决全局异常处理的问题。当然后续还出现了@RestControllerAdvice注解,其实这个注解与@ControllerAdvice注解的区别与联系就如同前面提到过的@Controller注解与@RestController注解是类似的,都是Response的返回值进行了序列化的处理。

Spring Boot中的异常分类

我们知道其实异常处理是Java语言本身的特性,所以Java语言本身就会有很多的异常,更不用说是在SpringBoot中存在的异常了,也是非常多的。

这里我们根据Spring Boot中的业务场景不同来对异常进行分类。一种就是进入业务之前的异常,一种就是在业务逻辑中的异常。如图所示,图片来源网络。

Spring Boot 进阶-Spring Boot的全局异常处理机制详解

在进入业务逻辑之前的异常一般是由Servlet操作引起的,一般需要处理的异常有如下一些

  • NoHandlerFoundException:表示客户端的请求在服务端没有找到对应的处理器,这个异常会抛出404的错误。
  • HttpRequestMethodNotSupportedException:从异常描述可以知道,它表示在使用HTTP请求的时候请求方法不支持,也就是说原本该使用GET请求的方法,使用了POST请求方法,这个异常会抛出405的错误
  • HttpMediaTypeNotSupportedException:这个异常表示客户端请求的媒体类型错误,也就是说原本需要使用JSON数据请求的却用的是x-form-data的数据进行请求。一般会抛出415的异常
  • MissingPathVariableException:表示路径参数未找到,一般就是参数传输错误,引起的错误就是400的错误。

而进入业务逻辑之后的异常则是有代码内部引起的一般常见的错误就是空指针异常。这个需要根据具体的业务逻辑以及对应的业务场景进行处理。

在Spring Boot中如何对这些异常进行统一处理呢?

在异常处理的时候一般开发人员关注的就是什么地方的异常,出现异常的原因是什么?在我们统一处理的时候需要注意哪些问题?下面我们就带着这些问题一一来解答。

统一异常处理的步骤很简单,我们还是以前面创建的项目来做演示

  • 第一步、需要创建一个全局异常处理的处理类
  • 第二步、在这个类上标注@RestControllerAdvice注解,或者使用@ControllerAdvice 和@ResponseBody组合来实现
  • 第三步、方法上标注上对应的异常处理的注解@ExceptionHandler,并且指定需要捕获的异常是什么,也可以指定多个。

根据以上的步骤我们来创建对应的处理代码。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(TestException.class)
    public String testExceptionHandler(TestException testException){
        log.info("全局异常处理",testException.getMessage());
        return "异常被全局捕获";
    }

}           

自定义异常

public class TestException extends Exception {

    //异常信息
    private String message;

    //构造函数
    public TestException(String message){
        super(message);
        this.message = message;
    }

    //获取异常信息,由于构造函数调用了super(message),不用重写此方法
    public String getMessage(){
        return message;
    }

}           

异常抛出类

public class UserInfoException {
    private String name;
    private String password;

    public UserInfoException(String name,String password){
        this.name = name;
        this.password = password;
    }

    public void throwException(String password) throws TestException{
        if (!this.password.equals(password)){
            throw new TestException("密码不正确!");
        }
    }
}
           

前端控制类

@RestController
public class HelloWorldController {

    @GetMapping("/hello")
    public String hello() throws TestException {
        UserInfoException ex = new UserInfoException("admin","123");
        ex.throwException("1231231");
        return "HelloWord";
    }

}           

通过这个例子,我们可以对全局的异常进行处理,在不同的地方采用同样的方式对抛出的异常进行捕获,并且按照自己的意愿将异常结果显示出来保证我们的程序正常运行的同时也可以让开发人员快速的查找定位问题。

根据上面的描述在全局异常捕获的时候可以配置多个异常使用同一个方法进行捕获,我们修改上面的代码如下

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(TestException.class)
    public String testExceptionHandler(TestException testException){
        log.info("全局异常处理",testException.getMessage());
        return "异常被全局捕获";
    }

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class,
            NoHandlerFoundException.class,
            MissingPathVariableException.class})
    public String reqeustException(Exception ex){
        log.info("测试请求类型错误");
        return "获取到请求异常";
    }
}           

从代码中我们可以看到在Handler处理方法上标注了多个注解类,那么这些注解类注解异常捕获的优先级又是什么呢?

异常捕获的优先级

对于前面提到的,有些业务异常我们是自定义的,而对于自定义的异常来讲,就需要继承Exception类。那么如果我们这里同时设置了父类和子类的异常捕获,到底是哪个类优先被捕获呢?

根据双亲委派机制可以知道,在我们类加载的时候先要在自己的加载器中找,如果找不到的情况下才会向父类或者父类的父类去找。那么异常匹配也是一样,首先会去匹配到自己的异常,如果没有进行处理才会去匹配父类的异常。也就是说在这里的异常捕获优先级是精确匹配的,就是说你设置了哪个哪个就会先被匹配。

可以通过查看org.springframework.web.method.annotation.ExceptionHandlerMethodResolver类的源码发现

/**
	 * Return the {@link Method} mapped to the given exception type, or
	 * {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none.
	 */
	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
		List<Class<? extends Throwable>> matches = new ArrayList<>();
    // 遍历所有异常处理的类
		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
			// 判断是否抛出异常的父类,如果是添加到集合中
      if (mappedException.isAssignableFrom(exceptionType)) {
				matches.add(mappedException);
			}
		}
    // 如果没有匹配则按照规则进行排序
		if (!matches.isEmpty()) {
			if (matches.size() > 1) {
				matches.sort(new ExceptionDepthComparator(exceptionType));
			}
      // 获取第一个进行抛出
			return this.mappedMethods.get(matches.get(0));
		}
		else {
			return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
		}
	}           

查看源码我们会发现其中有一步操作是对规则排序那么我们就来看看排序的规则是什么代码如下。会看到源码中一直递归找到其继承的父类。也就是说排序完成之后第一个获取到的规则就是它自己本身。

private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
		if (exceptionToMatch.equals(declaredException)) {
			// Found it!
			return depth;
		}
		// If we've gone as far as we can go and haven't found it...
		if (exceptionToMatch == Throwable.class) {
			return Integer.MAX_VALUE;
		}
		return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
	}           

总结

全局异常处理,在Spring Boot的开发中是经常会被用到,并且也极大的提升了开发效率。希望这篇文章能给大家带来帮助。对于源码级的分析,在后续深入了解Spring Boot原理的时候我们再去进行分析,这里只是浅析了一点源码,喜欢的读者可以关注笔者,后续还有很多干货带给大家。

继续阅读