详解SpringBoot如何优雅的进行全局异常处理

 更新时间:2023年07月02日 08:37:09   作者:码老思  
在SpringBoot的开发中,为了提高程序运行的鲁棒性,我们经常需要对各种程序异常进行处理,但是如果在每个出异常的地方进行单独处理的话,这会引入大量业务不相关的异常处理代码,这篇文章带大家了解一下如何优雅的进行全局异常处理

为了实现全局拦截,这里使用到了Spring中提供的两个注解,@RestControllerAdvice@ExceptionHandler,结合使用可以拦截程序中产生的异常,并且根据不同的异常类型分别处理。下面我会先介绍如何利用这两个注解,优雅的完成全局异常的处理,接着解释这背后的原理。

1. 如何实现全局拦截?

1.1 自定义异常处理类

在下面的例子中,我们继承了ResponseEntityExceptionHandler并使用@RestControllerAdvice注解了这个类,接着结合@ExceptionHandler针对不同的异常类型,来定义不同的异常处理方法。这里可以看到我处理的异常是自定义异常,后续我会展开介绍。

ResponseEntityExceptionHandler中包装了各种SpringMVC在处理请求时可能抛出的异常的处理,处理结果都是封装成一个ResponseEntity对象。ResponseEntityExceptionHandler是一个抽象类,通常我们需要定义一个用来处理异常的使用@RestControllerAdvice注解标注的异常处理类来继承自ResponseEntityExceptionHandler。ResponseEntityExceptionHandler中为每个异常的处理都单独定义了一个方法,如果默认的处理不能满足你的需求,则可以重写对某个异常的处理。

@Log4j2  
@RestControllerAdvice  
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {  
    /**  
     * 定义要捕获的异常 可以多个 @ExceptionHandler({})     *  
     * @param request  request  
     * @param e        exception  
     * @param response response  
     * @return 响应结果  
     */  
    @ExceptionHandler(AuroraRuntimeException.class)  
    public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {  
        AuroraRuntimeException exception = (AuroraRuntimeException) e;  
       if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) {  
           response.setStatus(HttpStatus.BAD_REQUEST.value());  
       } else if (exception.getCode() == ResponseCode.FORBIDDEN) {  
           response.setStatus(HttpStatus.FORBIDDEN.value());  
       } else {  
           response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());  
       }  
        return new GenericResponse(exception.getCode(), null, exception.getMessage());  
    }  
    @ExceptionHandler(NotLoginException.class)  
    public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {  
        log.error("token exception", e);  
        response.setStatus(HttpStatus.FORBIDDEN.value());  
        return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);  
    }  
}

1.2 定义异常码

这里定义了常见的几种异常码,主要用在抛出自定义异常时,对不同的情形进行区分。

@Getter  
public enum ResponseCode {  
    SUCCESS(0, "Success"),  
    INTERNAL_ERROR(1, "服务器内部错误"),  
    USER_INPUT_ERROR(2, "用户输入错误"),  
    AUTHENTICATION_NEEDED(3, "Token过期或无效"),  
    FORBIDDEN(4, "禁止访问"),  
    TOO_FREQUENT_VISIT(5, "访问太频繁,请休息一会儿");  
    private final int code;  
    private final String message;  
    private final Response.Status status;  
    ResponseCode(int code, String message, Response.Status status) {  
        this.code = code;  
        this.message = message;  
        this.status = status;  
    }  
    ResponseCode(int code, String message) {  
        this(code, message, Response.Status.INTERNAL_SERVER_ERROR);  
    }  
}

1.3 自定义异常类

这里我定义了一个AuroraRuntimeException的异常,就是在上面的异常处理函数中,用到的异常。每个异常实例会有一个对应的异常码,也就是前面刚定义好的。

@Getter  
public class AuroraRuntimeException extends RuntimeException {  
    private final ResponseCode code;  
    public AuroraRuntimeException() {  
        super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage()));  
        this.code = ResponseCode.INTERNAL_ERROR;  
    }  
    public AuroraRuntimeException(Throwable e) {  
        super(e);  
        this.code = ResponseCode.INTERNAL_ERROR;  
    }  
    public AuroraRuntimeException(String msg) {  
        this(ResponseCode.INTERNAL_ERROR, msg);  
    }  
    public AuroraRuntimeException(ResponseCode code) {  
        super(String.format("%s", code.getMessage()));  
        this.code = code;  
    }  
    public AuroraRuntimeException(ResponseCode code, String msg) {  
        super(msg);  
        this.code = code;  
    }  
}

1.4 自定义返回类型

为了保证各个接口的返回统一,这里专门定义了一个返回类型。

@Getter  
@Setter  
public class GenericResponse<T> {  
    private int code;  
    private T data;  
    private String message;  
    public GenericResponse() {};  
    public GenericResponse(int code, T data) {  
        this.code = code;  
        this.data = data;  
    }  
    public GenericResponse(int code, T data, String message) {  
        this(code, data);  
        this.message = message;  
    }  
    public GenericResponse(ResponseCode responseCode) {  
        this.code = responseCode.getCode();  
        this.data = null;  
        this.message = responseCode.getMessage();  
    }  
    public GenericResponse(ResponseCode responseCode, T data) {  
        this(responseCode);  
        this.data = data;  
    }  
    public GenericResponse(ResponseCode responseCode, T data, String message) {  
        this(responseCode, data);  
        this.message = message;  
    }  
}

实际测试异常

下面的例子中,我们想获取到用户的信息,如果用户的信息不存在,可以直接抛出一个异常,这个异常会被我们上面定义的全局异常处理方法所捕获,然后根据不同的异常编码,完成不同的处理和返回。

public User getUserInfo(Long userId) {  
	// some logic
    User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId);  
    if (user == null) {  
        throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "用户id不存在");  
    }
    // some logic
	....
}

以上就完成了整个全局异常的处理过程,接下来重点说说为什么@RestControllerAdvice@ExceptionHandler结合使用可以拦截程序中产生的异常?

全局拦截的背后原理?

下面会提到@ControllerAdvice注解,简单地说,@RestControllerAdvice与@ControllerAdvice的区别就和@RestController与@Controller的区别类似,@RestControllerAdvice注解包含了@ControllerAdvice注解和@ResponseBody注解。

接下来我们深入Spring源码,看看是怎么实现的,首先DispatcherServlet对象在创建时会初始化一系列的对象,这里重点关注函数initHandlerExceptionResolvers(context);.

public class DispatcherServlet extends FrameworkServlet {
    // ......
	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		// 重点关注
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}
    // ......
}

在initHandlerExceptionResolvers(context)方法中,会取得所有实现了HandlerExceptionResolver接口的bean并保存起来,其中就有一个类型为ExceptionHandlerExceptionResolver的bean,这个bean在应用启动过程中会获取所有被@ControllerAdvice注解标注的bean对象做进一步处理,关键代码在这里:

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
		implements ApplicationContextAware, InitializingBean {
    // ......
	private void initExceptionHandlerAdviceCache() {
		// ......
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		AnnotationAwareOrderComparator.sort(adviceBeans);
		for (ControllerAdviceBean adviceBean : adviceBeans) {
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
			if (resolver.hasExceptionMappings()) {
			    // 找到所有ExceptionHandler标注的方法并保存成一个ExceptionHandlerMethodResolver类型的对象缓存起来
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
				if (logger.isInfoEnabled()) {
					logger.info("Detected @ExceptionHandler methods in " + adviceBean);
				}
			}
			// ......
		}
	}
    // ......
}

当Controller抛出异常时,DispatcherServlet通过ExceptionHandlerExceptionResolver来解析异常,而ExceptionHandlerExceptionResolver又通过ExceptionHandlerMethodResolver 来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适用的@ExceptionHandler标注的方法是这里:

public class ExceptionHandlerMethodResolver {
	// ......
	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
		List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
		// 找到所有适用于Controller抛出异常的处理方法,例如Controller抛出的异常
		// 是AuroraRuntimeException(继承自RuntimeException),那么@ExceptionHandler(AuroraRuntimeException.class)和
		// @ExceptionHandler(Exception.class)标注的方法都适用此异常
		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
			if (mappedException.isAssignableFrom(exceptionType)) {
				matches.add(mappedException);
			}
		}
		if (!matches.isEmpty()) {
		/* 这里通过排序找到最适用的方法,排序的规则依据抛出异常相对于声明异常的深度,例如
	Controller抛出的异常是是AuroraRuntimeException(继承自RuntimeException),那么AuroraRuntimeException
	相对于@ExceptionHandler(AuroraRuntimeException.class)声明的AuroraRuntimeException.class其深度是0,
	相对于@ExceptionHandler(Exception.class)声明的Exception.class其深度是2,所以
	@ExceptionHandler(BizException.class)标注的方法会排在前面 */
			Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
			return this.mappedMethods.get(matches.get(0));
		}
		else {
			return null;
		}
	}
    // ......
}

整个@RestControllerAdvice处理的流程就是这样,结合@ExceptionHandler就完成了对不同异常的灵活处理。

以上就是详解SpringBoot如何优雅的进行全局异常处理的详细内容,更多关于SpringBoot 全局异常处理的资料请关注脚本之家其它相关文章!

相关文章

  • Java网络编程之入门篇

    Java网络编程之入门篇

    这篇文章主要介绍了Java网络编程入门,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • JFinal极速开发框架使用笔记分享

    JFinal极速开发框架使用笔记分享

    下面小编就为大家分享一篇JFinal极速开发框架使用笔记,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-01-01
  • SpringCloud网关组件Gateway原理深度解析

    SpringCloud网关组件Gateway原理深度解析

    Spring Cloud Gateway是Spring Cloud微服务生态下的网关组件,一些基础的请求预处理的逻辑可以统一实现在网关这一层,这样业务服务只需要专注于处理业务逻辑即可,所以本文就带大家深度解析网关组件Gateway,需要的朋友可以参考下
    2023-07-07
  • 实例讲解Java设计模式编程中的OCP开闭原则

    实例讲解Java设计模式编程中的OCP开闭原则

    这篇文章主要介绍了Java设计模式编程中的开闭原则,开闭原则的大意被作者总结为用抽象构建框架,用实现扩展细节,需要的朋友可以参考下
    2016-02-02
  • Java8 HashMap遍历方式性能探讨

    Java8 HashMap遍历方式性能探讨

    JDK8之前,可以使用keySet或者entrySet来遍历HashMap,JDK8中引入了map.foreach来进行遍历
    2021-09-09
  • java网上图书商城(2)Category模块

    java网上图书商城(2)Category模块

    这篇文章主要介绍了java网上图书商城,Category模块,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • 详解spring cloud构建微服务架构的网关(API GateWay)

    详解spring cloud构建微服务架构的网关(API GateWay)

    这篇文章主要介绍了详解spring cloud构建微服务架构的网关(API GateWay),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • Springboot实现动态定时任务管理的示例代码

    Springboot实现动态定时任务管理的示例代码

    最近在做spring boot项目开发中,由于使用@EnableScheduling注解和@Scheduled注解来实现的定时任务,只能静态的创建定时任务,不能动态修改、添加、删除、启/停任务,下面通过本文给大家介绍Springboot实现动态定时任务管理的方法,感兴趣的朋友跟随小编一起看看吧
    2023-07-07
  • java实现置换密码加密解密

    java实现置换密码加密解密

    这篇文章主要为大家详细介绍了java实现置换密码加密解密,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-03-03
  • java中Optional的使用详细解析

    java中Optional的使用详细解析

    这篇文章主要介绍了java新特性之Optional的详细解析,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04

最新评论