全局异常处理及自定义异常:ErrorController与@ControllerAdvice区别和用法
参考资料
- springboot继承AbstractErrorController实现全局的异常处理
https://blog.csdn.net/qq_29684305/article/details/82286469- spring boot 原生错误处理ErrorController
https://blog.csdn.net/shenyunsese/article/details/53390116- @ControllerAdvice 拦截异常并统一处理
https://my.oschina.net/langwanghuangshifu/blog/2246890
ErrorController
在springboot项目中当我们访问一个不存在的url时经常会出现以下页面

在postman访问时则是以下情况

image
对于上面的情况究竟是什么原因造成呢,实际上当springboot项目出现异常时,默认会跳转到/error,而/error则是由BasicErrorController进行处理,其代码如下
@Controller@RequestMapping({"${server.error.path:${error.path:/error}}"})public class BasicErrorController extends AbstractErrorController {private final ErrorProperties errorProperties;public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {this(errorAttributes, errorProperties, Collections.emptyList());}public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {super(errorAttributes, errorViewResolvers);Assert.notNull(errorProperties, "ErrorProperties must not be null");this.errorProperties = errorProperties;}public String getErrorPath() {return this.errorProperties.getPath();}@RequestMapping(produces = {"text/html"})public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {HttpStatus status = this.getStatus(request);Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));response.setStatus(status.value());ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);return modelAndView == null ? new ModelAndView("error", model) : modelAndView;}@RequestMapping@ResponseBodypublic ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));HttpStatus status = this.getStatus(request);return new ResponseEntity(body, status);}protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {IncludeStacktrace include = this.getErrorProperties().getIncludeStacktrace();if (include == IncludeStacktrace.ALWAYS) {return true;} else {return include == IncludeStacktrace.ON_TRACE_PARAM ? this.getTraceParameter(request) : false;}}protected ErrorProperties getErrorProperties() {return this.errorProperties;}}
可见BasicErrorController是一个控制器,对/error进行处理BasicErrorController根据Accept头的内容,输出不同格式的错误响应。比如针对浏览器的请求生成html页面,针对其它请求生成json格式的返回。字段为accept的text/html的内容来判断我们也可自定义ErrorController来实现自己对错误的处理,例如浏览器访问也返回json字符串(返回text/html),或自定义错误页面,不同status跳转不同的页面等,同时其他请求也可自定义返回的json格式
下面是自己写的一个ErrorController
import java.util.HashMap;import java.util.Map;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import com.alibaba.fastjson.JSONObject;import com.xuecheng.framework.model.response.ErrorCode;import com.xuecheng.framework.model.response.ResultCode;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.web.servlet.error.ErrorAttributes;import org.springframework.boot.web.servlet.error.ErrorController;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/*** web错误 全局处理* @author jiangwf**/import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.context.request.ServletWebRequest;@Controllerpublic class InterfaceErrorController implements ErrorController {private static final String ERROR_PATH="/error";private ErrorAttributes errorAttributes;@Overridepublic String getErrorPath() {return ERROR_PATH;}@Autowiredpublic InterfaceErrorController(ErrorAttributes errorAttributes) {this.errorAttributes=errorAttributes;}/*** web页面错误处理*/@RequestMapping(value=ERROR_PATH,produces="text/html")@ResponseBodypublic String errorPageHandler(HttpServletRequest request,HttpServletResponse response) {ServletWebRequest requestAttributes = new ServletWebRequest(request);Map<String, Object> attr = this.errorAttributes.getErrorAttributes(requestAttributes, false);JSONObject jsonObject = new JSONObject();ErrorCode errorCode = new ErrorCode(false, (int) attr.get("status"), (String) attr.get("message"));return JSONObject.toJSONString(errorCode);}/*** 除web页面外的错误处理,比如json/xml等*/@RequestMapping(value=ERROR_PATH)@ResponseBodypublic ResultCode errorApiHander(HttpServletRequest request) {ServletWebRequest requestAttributes = new ServletWebRequest(request);Map<String, Object> attr=this.errorAttributes.getErrorAttributes(requestAttributes, false);return new ErrorCode(false, (int)attr.get("status"), (String) attr.get("message"));}}
当是浏览器访问时返回json字符串
image当是其他请求时返回自定义的ErrorCode
imageErrorCode代码如下
import lombok.AllArgsConstructor;import lombok.Data;import lombok.ToString;@ToString@Data@AllArgsConstructorpublic class ErrorCode implements ResultCode{private boolean success;private int code;private String message;@Overridepublic boolean success() {return false;}@Overridepublic int code() {return 0;}@Overridepublic String message() {return null;}}
ResultCode代码如下
public interface ResultCode {//操作是否成功,true为成功,false操作失败boolean success();//操作代码int code();//提示信息String message();}
@ControllerAdvice
上面我们提到ErrorController可对全局错误进行处理,但是其获取不到异常的具体信息,同时也无法根据异常类型进行不同的响应,例如对自定义异常的处理而@ControllerAdvice可对全局异常进行捕获,包括自定义异常需要清楚的是,其是应用于对springmvc中的控制器抛出的异常进行处理,而对于404这样不会进入控制器处理的异常不起作用,所以此时还是要依靠ErrorController来处理
问题:
实际上,当出现错误,如获取值为空或出现异常时,我们并不希望用户看到异常的具体信息,而是希望对对应的错误和异常做相应提示在MVC框架中很多时候会出现执行异常,那我们就需要加try/catch进行捕获,如果service层和controller层都加上,那就会造成代码冗余
解决方法:
统一返回的数据格式,如上的ResultCode,可实现其做更多扩展,对于程序的可预知错误,我们采取抛出异常的方式,再统一处理我们在编程时的顺序是先校验判断,有问题则抛出异常信息,最后执行具体的业务操作,返回成功信息在统一异常处理类中去捕获异常,无需再代码中try/catch,向用户返回统一规范的响应信息
异常处理流程
系统对异常的处理使用统一的异常处理流程:
自定义异常类型自定义错误代码及错误信息对于可预知的异常由程序员在代码中主动抛出,有SpringMVC统一捕获
可预知异常是程序员在代码中手动抛出本系统定义的特点异常类型,由于是程序员抛出的异常,通常异常信息比较齐全,程序员在抛出时会指定错误代码及错误信息,获取异常信息也比较方便对于不可预知的异常(运行时异常)由SpringMVC统一捕获Exception类型的异常
不可预知的异常通常是由于系统出现bug、或一些不可抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)可预知异常及不可预知异常最终都会采用统一的信息格式(错误代码+错误信息)来表示,最终也会随请求响应给客户端
异常抛出及处理流程

image
在controller、service、dao中程序员抛出自定义异常;SpringMVC框架抛出框架异常类型统一由异常捕获类捕获异常并进行处理捕获到自定义异常则直接取出错误代码及错误信息,响应给用户捕获到非自定义异常类型首先从Map中找该异常类型是否对应具体的错误代码,如果有则取出错误代码和错误信息并响应给用户,如果从Map中占不到异常类型所对应的错误代码则统一为99999错误代码并响应给用户将错误代码及错误信息以json格式响应给用户
下面就开始我们的异常处理编程
一、可预知异常
自定义异常类
import com.xuecheng.framework.model.response.ResultCode;import jdk.nashorn.internal.objects.annotations.Getter;/*** @Author: jiangweifan* @Date: 2019/3/4 20:06* @Description:*/public class CustomException extends RuntimeException {private ResultCode resultCode;public CustomException(ResultCode resultCode) {super("错误代码:" + resultCode.code()+" 错误信息:" + resultCode.message());this.resultCode = resultCode;}public ResultCode getResultCode() {return resultCode;}}
自定义异常抛出类
import com.xuecheng.framework.model.response.ResultCode;/*** @Author: jiangweifan* @Date: 2019/3/4 20:09* @Description:*/public class ExceptionCast {public static void cast(ResultCode resultCode, boolean condition) {if (condition) {throw new CustomException(resultCode);}}}
异常捕获类
使用@ControllerAdvice和@ExceptionHandler注解来捕获指定类型的异常
import com.google.common.collect.ImmutableMap;import com.xuecheng.framework.model.response.CommonCode;import com.xuecheng.framework.model.response.ResponseResult;import com.xuecheng.framework.model.response.ResultCode;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import java.net.SocketTimeoutException;/*** @Author: jiangweifan* @Date: 2019/3/4 20:13* @Description:*/@ControllerAdvice@Slf4jpublic class ExceptionCatch {@ExceptionHandler(CustomException.class)@ResponseBodypublic ResponseResult customException(CustomException e) {log.error("catch exception : {} \r\nexception", e.getMessage(), e);ResponseResult responseResult = new ResponseResult(e.getResultCode());return responseResult;}}
4.1 定义响应数据格式
import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;/*** @Author: mrt.* @Description:* @Date:Created in 2018/1/24 18:33.* @Modified By:*/@Data@ToString@NoArgsConstructorpublic class ResponseResult implements Response {//操作是否成功boolean success = SUCCESS;//操作代码int code = SUCCESS_CODE;//提示信息String message;public ResponseResult(ResultCode resultCode){this.success = resultCode.success();this.code = resultCode.code();this.message = resultCode.message();}public static ResponseResult SUCCESS(){return new ResponseResult(CommonCode.SUCCESS);}public static ResponseResult FAIL(){return new ResponseResult(CommonCode.FAIL);}}其中Response代码如下public interface Response {public static final boolean SUCCESS = true;public static final int SUCCESS_CODE = 10000;}
4.2 定义错误代码(ResultCode上文已给出)
import com.xuecheng.framework.model.response.ResultCode;import lombok.ToString;/*** Created by mrt on 2018/3/5.*/@ToStringpublic enum CmsCode implements ResultCode {CMS_ADDPAGE_EXISTSNAME(false,24001,"页面名称已存在!"),CMS_GENERATEHTML_DATAURLISNULL(false,24002,"从页面信息中找不到获取数据的url!"),CMS_GENERATEHTML_DATAISNULL(false,24003,"根据页面的数据url获取不到数据!"),CMS_GENERATEHTML_TEMPLATEISNULL(false,24004,"页面模板为空!"),CMS_GENERATEHTML_HTMLISNULL(false,24005,"生成的静态html为空!"),CMS_GENERATEHTML_SAVEHTMLERROR(false,24005,"保存静态html出错!"),CMS_COURSE_PERVIEWISNULL(false,24007,"预览页面为空!"),CMS_TEMPLATEFILE_ERROR(false,24008,"模板文件需要.ftl后缀!"),CMS_TEMPLATEFILE_NULL(false,24009,"模板文件为空!"),CMS_TEMPLATEFILE_EXCEPTION(false,24010,"解析模板文件异常!"),CMS_TEMPLATEFILE_FAIL(false,24011,"模板文件存储失败!"),CMS_TEMPLATEFILE_DELETE_ERROR(false,24012,"模板文件删除失败!"),CMS_Config_NOTEXISTS(false,24013,"不存在该数据模型!"),CMS_PAGE_NULL(false,24014,"不存在该页面数据!"),CMS_GENERATEHTML_CONTENT_FAIL(false,24014,"获取页面模板失败!");//操作代码boolean success;//操作代码int code;//提示信息String message;private CmsCode(boolean success, int code, String message){this.success = success;this.code = code;this.message = message;}@Overridepublic boolean success() {return success;}@Overridepublic int code() {return code;}@Overridepublic String message() {return message;}}
在方法中抛出异常进行测试
@GetMapping("/list/{page}/{size}")public QueryResponseResult findList(@PathVariable("page") int page, @PathVariable("size")int size, QueryPageRequest queryPageRequest) {ExceptionCast.cast(CmsCode.CMS_COURSE_PERVIEWISNULL, queryPageRequest == null);return pageService.findList(page,size,queryPageRequest);}
最终方法得到以下结果

image
二、不可预知异常处理
在异常捕获类中添加对Exception类型异常的捕获,完整代码如下
import com.google.common.collect.ImmutableMap;import com.xuecheng.framework.model.response.CommonCode;import com.xuecheng.framework.model.response.ResponseResult;import com.xuecheng.framework.model.response.ResultCode;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import java.net.SocketTimeoutException;/*** @Author: jiangweifan* @Date: 2019/3/4 20:13* @Description:*/@ControllerAdvice@Slf4jpublic class ExceptionCatch {//使用EXCEPTIOS存放异常类型 和错误代码的映射,ImmutableMap的特点是已创建就不可变,并且线程安全private static ImmutableMap<Class<? extends Throwable>, ResultCode> EXCEPTIOS;//是由builder来构建一个异常类型和错误代码的映射private static ImmutableMap.Builder<Class<? extends Throwable>, ResultCode> builder =ImmutableMap.builder();static {//初始化基础类型异常与错误代码的映射builder.put(NullPointerException.class, CommonCode.NULL);builder.put(SocketTimeoutException.class, CommonCode.NULL);}@ExceptionHandler(CustomException.class)@ResponseBodypublic ResponseResult customException(CustomException e) {log.error("catch exception : {} \r\nexception", e.getMessage(), e);ResponseResult responseResult = new ResponseResult(e.getResultCode());return responseResult;}@ExceptionHandler(Exception.class)@ResponseBodypublic ResponseResult exception(Exception e) {log.error("catch exception : {} \r\nexception", e.getMessage(), e);if (EXCEPTIOS == null) {EXCEPTIOS = builder.build();}final ResultCode resultCode = EXCEPTIOS.get(e.getClass());if (resultCode != null) {return new ResponseResult(resultCode);} else {return new ResponseResult(CommonCode.SERVER_ERROR);}}}
对于不可预知异常的处理,我们采取先从定义好的Map获取该异常类型对应的错误代码和错误信息,若没有则统一返回CommonCode.SERVER_ERROR对于CommonCode代码如下(ResultCode上文已给出)
import lombok.ToString;/*** @Author: mrt.* @Description:* @Date:Created in 2018/1/24 18:33.* @Modified By:*/@ToStringpublic enum CommonCode implements ResultCode{SUCCESS(true,10000,"操作成功!"),FAIL(false,19999,"操作失败!"),UNAUTHENTICATED(false,10001,"此操作需要登陆系统!"),UNAUTHORISE(false,10002,"权限不足,无权操作!"),NULL(false,10003,"空值异常!"),TIMEOUT(false, 10004, "服务器连接超时!"),SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!");// private static ImmutableMap<Integer, CommonCode> codes ;//操作是否成功boolean success;//操作代码int code;//提示信息String message;private CommonCode(boolean success,int code, String message){this.success = success;this.code = code;this.message = message;}@Overridepublic boolean success() {return success;}@Overridepublic int code() {return code;}@Overridepublic String message() {return message;}}
方法中进行测试
@GetMapping("/list/{page}/{size}")public QueryResponseResult findList(@PathVariable("page") int page, @PathVariable("size")int size, QueryPageRequest queryPageRequest) {int a= 1/0;return pageService.findList(page,size,queryPageRequest);}
浏览器访问结果如下:

image
至此我们完成了对全局异常的处理
欢迎关注公众号,后续文章更新通知,一起讨论技术问题 。
赞 (0)
