springboot-异常处理-异常捕获规范

背景

必看!!减少 try catch ,可以这样干

软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的try {...} catch {...} finally {...} 代码块,不仅有大量的冗余代码,而且还影响代码的可读性

既然想要业务代码不显式地对异常进行捕获、处理,而异常肯定还是处理的,不然系统岂不是动不动就崩溃了,所以必须得有其他地方捕获并处理这些异常。

自定义异常错误页面

默认情况下,在遇到异常时,SpringBoot 会自动跳到一个统一的异常页面,Spring Boot提供/error处理所有错误的映射
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据

SpringBoot 默认的异常处理机制:一旦程序中出现了异常 SpringBoot 就会请求 /error 的 url 。在 SpringBoot 中提供了一个叫 BasicErrorController 来处理 /error 请求,然后跳转到默认显示异常的页面来展示异常信息。
在这里插入图片描述
在这里插入图片描述

接下来就是自定义异常错误页面了,方法很简单,就是在目录 src/main/resources/templates/ 下定义一个叫 error 的文件,可以是 jsp 也可以是 html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>自定义 springboot 异常处理页面</title>
</head>
<body>
Springboot BasicExceptionController  错误页面
<br>
<span th:text="${msg}"></span>
</body>
</html>

在这里插入图片描述

也可以在error文件夹下放相应状态码的页面,error/下的4xx,5xx页面会被自动解析,有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
在这里插入图片描述

error/下的4xx,5xx页面会被自动解析原理

系统默认的异常解析器为 DefaultErrorAttributes
在这里插入图片描述

  • DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null

  • 默认没有任何人能处理异常,所以异常会被抛出

    • 如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
    • 解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
      在这里插入图片描述
  • 默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html

    优先级
    * <li>{@code '/<templates>/error/404.<ext>'}</li>
    * <li>{@code '/<static>/error/404.html'}</li>
    * <li>{@code '/<templates>/error/4xx.<ext>'}</li>
    * <li>{@code '/<static>/error/4xx.html'}</li>
    
    DefaultErrorViewResolver 源码,会拼接出 error/404.html 访问路径
    
    private ModelAndView resolve(String viewName, Map<String, Object> model) {
    	String errorViewName = "error/" + viewName;
    	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
    			this.applicationContext);
    	if (provider != null) {
    		return new ModelAndView(errorViewName, model);
    	}
    	return resolveResource(errorViewName, model);
    }
    
    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
    	for (String location : this.resources.getStaticLocations()) {
    		try {
    			Resource resource = this.applicationContext.getResource(location);
    			resource = resource.createRelative(viewName + ".html");
    			if (resource.exists()) {
    				return new ModelAndView(new HtmlResourceView(resource), model);
    			}
    		}
    		catch (Exception ex) {
    		}
    	}
    	return null;
    }
    
  • 模板引擎最终响应这个页面 error/500.html

统一异常处理

Spring3.2版本增加了一个注解@ControllerAdvice,可以与@ExceptionHandler@InitBinder@ModelAttribute 等注解注解配套使用

跟异常处理相关的只有注解@ExceptionHandler,从字面上看,就是 异常处理器 的意思,其实际作用也是:

若在某个 Controller类 定义一个 异常处理方法
并 在方法上添加该注解 
那么当 出现指定的异常 时
执行该处理异常的方法

可以使用springmvc提供的数据绑定,比如注入HttpServletRequest等,还可以接受一个当前抛出的Throwable对象

但是,这样就必须在每一个Controller类都定义一套这样的异常处理方法,因为异常可以是各种各样。这样就会造成大量的冗余代码,而且若需要新增一种异常的处理逻辑,就必须修改所有Controller类了,很不优雅。


当然你可能会说,那就定义个类似BaseController的基类,这样总行了吧。

这种做法虽然没错,但仍不尽善尽美,因为这样的代码有一定的侵入性和耦合性。简简单单的Controller,我为啥非得继承这样一个类呢,万一已经继承其他基类了呢。大家都知道Java只能继承一个类

只使用 @ExceptionHandler 注解处理局部异常

使用这个注解就容易了,但是只能处理使用 @ExceptionHandler 注解的方法的 Controller 的异常,对于其他 Controller 的异常就无能为力了,只能再使用同样的方法将使用 @ExceptionHandler 注解的方法写入要捕获异常的 Controller 中,所以不推荐使用

使用方式:在ExceptionController加入使用 @ExceptionHandler 注解的方法代码,整个 ExceptionController 代码如下:

@Controller
public class ExceptionController {
	/**
	 * 描述:捕获 ArithmeticException 异常
	 * @param model 将Model对象注入到方法中
	 * @param e 将产生异常对象注入到方法中
	 * @return 指定错误页面
	 */
	@ExceptionHandler(value = {ArithmeticException.class})
	public String arithmeticExceptionHandle(Model model, Exception e) {
		
		model.addAttribute("msg", "@ExceptionHandler" + e.getMessage());
		log.info(e.getMessage());
		
		return "error";
	}
	
	/**
     * 服务器异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public R<String> exception(Exception e) {
        log.error("服务器异常! msg: -> ", e);
        return R.failed("服务器异常!");
    }
}

代码说明:
注解 @ExceptionHandlervalue 的值为数组,表示指定捕获的异常类型,这里表示捕获 ArithmeticException 异常,跳转的页面为统一的 error.html 页面model.addAttribute("msg", "@ExceptionHandler" + e.getMessage())用来区分是 SpringBoot 处理的异常还是我们自己的方法处理的异常,也是使用这个方式来区分。

@ExceptionHandler(value = MyException.class) -- 注解类型
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) -- 错误码
@ResponseBody --  返回json

使用 @ControllerAdvice + @ExceptionHandler 注解处理全局异常

在这里插入图片描述

那有没有一种方案,既不需要跟Controller耦合,也可以将定义的 异常处理器 应用到所有控制器呢?所以注解@ControllerAdvice出现了,简单的说,该注解可以把异常处理器应用到所有控制器,而不是单个控制器

借助该注解,我们可以实现:在独立的某个地方,比如单独一个类,定义一套对各种异常的处理机制,然后在类的签名加上注解@ControllerAdvice,统一对 不同阶段的、不同异常 进行处理。这就是统一异常处理的原理。


使用 @ControllerAdvice +@ExceptionHandler注解能够处理全局异常,这种方式推荐使用,可以根据不同的异常对不同的异常进行处理。底层是 ExceptionHandlerExceptionResolver 支持

如果需要处理其他异常,例如 NullPointerException 异常,则只需要在 GlobalException 类中定义一个方法使用 @ExceptionHandler(value = {NullPointerException.class}) 注解该方法,在该方法内部处理异常就可以了。

使用方式:定义一个类,使用 @ControllerAdvice 注解该类,使用 @ExceptionHandler 注解方法,这里定义了一个 GlobalException 类表示来处理全局异常,代码如下:

@ControllerAdvice
//或者 @RestControllerAdvice 内部有 @ControllerAdvice 注解
@Order(Ordered.HIGHEST_PRECEDENCE)    //代表这个过滤器在众多过滤器中级别最高,也就是过滤的时候最先执行
public class GlobalException {	
	/**
	 * 描述:捕获 ArithmeticException 异常
	 * @param model 将Model对象注入到方法中
	 * @param e 将产生异常对象注入到方法中
	 * @return 指定错误页面
	 */
	@ExceptionHandler(value = {ArithmeticException.class})
	public String arithmeticExceptionHandle(Model model, Exception e) { 
		model.addAttribute("msg", "@ControllerAdvice + @ExceptionHandler :" + e.getMessage());
		log.info(e.getMessage()); 
		return "error";
	}
	
	/**
     * 服务器异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public R<String> exception(Exception e) {
        log.error("服务器异常! msg: -> ", e);
        return R.failed("服务器异常!");
    }
}

实现 HandlerExceptionResolver 接口处理异常

实现 HandlerExceptionResolver 重写 resolveException 方法处理异常;可以作为默认的全局异常处理规则

在这里插入图片描述

注意:在类上加上 @Configuration 注解

@Configuration
@Order(value= Ordered.HIGHEST_PRECEDENCE)  //优先级,数字越小优先级越高
public class HandlerExceptionResolverImpl implements HandlerExceptionResolver {

	@Override
	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
			Exception ex) {
		ModelAndView modelAndView = new ModelAndView();
		
		modelAndView.addObject("msg", "实现 HandlerExceptionResolver 接口处理异常");
		
		//判断不同异常类型,做不同视图跳转
		if(ex instanceof ArithmeticException){
			modelAndView.setViewName("error");
		}
		
		return modelAndView;
	}

}

通过ErrorViewResolver 实现自定义处理异常,原理:

  • basicErrorController 要去的页面地址是ErrorViewResolver
  • 自己调用response.error,请求也会被转发给basicErrorController进行处理。
  • 如果自己没有调用,并且异常没有任何人能够处理,tomact底层会自动调用response.sendError将请求转发给basicErrorController处理,里面使用了ErrorViewResolver进行解析ErrorViewResolver转到一个页面或者返回json数据

@ResponseStatus+自定义异常

底层是ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason)

@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {

    public  UserTooManyException(){

    }
    public  UserTooManyException(String message){
        super(message);
    }
}
使用时直接throw new UserTooManyException()

异常捕获规范

捕获异常

throw 方式

通过 e instanceof BusinessException 来进行区别抛出提示

try 里面的通过判断,进行自定义 throw,具体的就没写,如果想要再添加异常,则必须在 try 里面 throw,catch 才能够捕获

// try 里面抛了两个自定义异常
// catch 里面进行捕获,根据类型返回不同的提示消息
 
 
// 这里的 BusinessException 异常是添加失败
// 这里的 Exception 异常是数据库异常或者表不存在又或者表被改名了......
try {
    throw new BusinessException("添加失败!");
    // throw new Exception("数据库异常!");
} catch (Exception e) {
    if (e instanceof BusinessException) {
        throw new Exception(e.getMessage());
    } else if (e instanceof Exception) {
        throw new BusinessException(msg);
    }
}
 
// 我的 catch 里面 throw 不一样是因为 BusinessException 里面有自定义拼接的通用字符串。
// 这里面可以一样:catch           里面的                throw

注释的那个异常是写不写都一样(前提是出现这个异常),都能捕获得到,就是都能够进 catch

try 里面的通过判断,进行自定义 throw,上面我就没写。

通过 try {} catch () {} catch () {} 来进行区别抛出提示(建议此种方法)

catch里面写异常,大括号里面进行处理,切记,最里面的catch异常是最小的,先执行第一个catch,如果没有才会执行第二个catch,从第一个到后面的等级越来越大。

try {
    throw new BusinessException("添加失败!");
    // throw new Exception("数据库异常!");
} catch (BusinessException e) {
    String msg = "添加失败";
    throw new BusinessException(msg); // throw new Exception(msg)
} catch (Exception e) {
    String msg = "数据库异常!";
    throw new Exception(msg); // throw new BusinessException(msg)
}
 
// throw 自定义异常根据公司业务需求自定义选择,这里仅仅列举了两个

需要注意的是,无论哪一种方式,当程序处理运行异常时候,只能进入特定的一个,如果你想要进入自定义异常,在 try 里面必须抛出来,才会捕获到自定义的异常!!!!其实,换句话说,这篇文章就是讲这个多情况异常的,肯定是需要自定义的,所以 try 里面肯定会抛异常的,只不过根据自己的业务去定具体需要抛几个的问题而已。

获取异常类型:e.getClass()

获取异常信息:e.getMessage()

在 service 里面实现的方法里面进行业务处理,进行 try catch,要注意的是,一般来说都会用一个 try 把代码全部包起来,如果有多个业务处理,需要进行区分,就多写 catch,叫做 try 不够,用 catch 来补

提示消息:是把 msg 以参数的形式抛给上一层,即放在自定义的异常里面,action 获取到的就是 msg,这一步做到了把代码的异常错误消息,转为自定义提示消息

示例

报错

在这里插入图片描述

分析

public X todo(xxxx) throws BusinessException {
	try {
		return xxxxx;
	} catch (ApiException e) {
		String message = e.getMessage();
		//	这里 拿到报错的 message ,如果是 conflict 就上抛异常,异常类型为  BusinessException 
		if ("Conflict".equalsIgnoreCase(message)) {
			throw 异常.buildException();
		}
		log.error("xxxx", e);
		throw 异常.buildException();
	}
}
public String xxxx(xxxx param) {
	try {
		//方法调用
		todo(....);
		if (初步判断) {
			throw 异常.buildException();
		}
	} catch (DuplicateKeyException e) {
		throw 异常.buildException();
	} catch (FeignException.BadRequest e) {
		log.error("xxxx", e);
		throw 异常.buildException();
	} catch (Exception e) {
		try {
		  	//方法调用
		} catch (Exception e1) {
			log.error("xxxx", e1);
		}
		// 捕获到抛出的 BusinessException ,抛出 e ,这里 e 就是 todo 中 throw 的 e 
		if (e instanceof BusinessException) {
			throw e;
		}
		throw 异常.buildException();
	}
	return xxxxx;
}

其他

通常情况下,我们都会定义一个全局异常处理类来处理异常,但是当我们定义了多个异常处理类,我们如何去保证它的执行顺序呢?

可以通过@Order注解来设置处理器的捕获执行顺序,如果没有该注解,他就会使用默认值,也就是Ordered.LOWEST_PRECEDENCE

越小的优先执行,所以我们只需要在全局参数异常处理器加上 @Order(Ordered.LOWEST_PRECEDENCE - 2)就能保证它的执行顺序优先于全局异常处理器

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值