全局异常处理及自定义异常:ErrorController与@ControllerAdvice区别和用法

参考资料

  1. springboot继承AbstractErrorController实现全局的异常处理
    https://blog.csdn.net/qq_29684305/article/details/82286469
  2. spring boot 原生错误处理ErrorController
    https://blog.csdn.net/shenyunsese/article/details/53390116
  3. @ControllerAdvice 拦截异常并统一处理
    https://my.oschina.net/langwanghuangshifu/blog/2246890

ErrorController

在springboot项目中当我们访问一个不存在的url时经常会出现以下页面

在postman访问时则是以下情况

image

对于上面的情况究竟是什么原因造成呢,实际上当springboot项目出现异常时,默认会跳转到/error,而/error则是由BasicErrorController进行处理,其代码如下

  1. @Controller
  2. @RequestMapping({"${server.error.path:${error.path:/error}}"})
  3. public class BasicErrorController extends AbstractErrorController {
  4.     private final ErrorProperties errorProperties;
  5.     public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
  6.         this(errorAttributes, errorProperties, Collections.emptyList());
  7.     }
  8.     public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
  9.         super(errorAttributes, errorViewResolvers);
  10.         Assert.notNull(errorProperties, "ErrorProperties must not be null");
  11.         this.errorProperties = errorProperties;
  12.     }
  13.     public String getErrorPath() {
  14.         return this.errorProperties.getPath();
  15.     }
  16.     @RequestMapping(
  17.         produces = {"text/html"}
  18.     )
  19.     public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  20.         HttpStatus status = this.getStatus(request);
  21.         Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
  22.         response.setStatus(status.value());
  23.         ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
  24.         return modelAndView == null ? new ModelAndView("error", model) : modelAndView;
  25.     }
  26.     @RequestMapping
  27.     @ResponseBody
  28.     public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
  29.         Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
  30.         HttpStatus status = this.getStatus(request);
  31.         return new ResponseEntity(body, status);
  32.     }
  33.     protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
  34.         IncludeStacktrace include = this.getErrorProperties().getIncludeStacktrace();
  35.         if (include == IncludeStacktrace.ALWAYS) {
  36.             return true;
  37.         } else {
  38.             return include == IncludeStacktrace.ON_TRACE_PARAM ? this.getTraceParameter(request) : false;
  39.         }
  40.     }
  41.     protected ErrorProperties getErrorProperties() {
  42.         return this.errorProperties;
  43.     }
  44. }
  • 可见BasicErrorController是一个控制器,对/error进行处理
  • BasicErrorController根据Accept头的内容,输出不同格式的错误响应。比如针对浏览器的请求生成html页面,针对其它请求生成json格式的返回。字段为accept的text/html的内容来判断
  • 我们也可自定义ErrorController来实现自己对错误的处理,例如浏览器访问也返回json字符串(返回text/html),或自定义错误页面,不同status跳转不同的页面等,同时其他请求也可自定义返回的json格式

下面是自己写的一个ErrorController

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import javax.servlet.http.HttpServletRequest;
  4. import javax.servlet.http.HttpServletResponse;
  5. import com.alibaba.fastjson.JSONObject;
  6. import com.xuecheng.framework.model.response.ErrorCode;
  7. import com.xuecheng.framework.model.response.ResultCode;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.boot.web.servlet.error.ErrorAttributes;
  10. import org.springframework.boot.web.servlet.error.ErrorController;
  11. import org.springframework.stereotype.Controller;
  12. import org.springframework.web.bind.annotation.RequestMapping;
  13. /**
  14.  * web错误 全局处理
  15.  * @author jiangwf
  16.  *
  17.  */
  18. import org.springframework.web.bind.annotation.ResponseBody;
  19. import org.springframework.web.context.request.ServletWebRequest;
  20. @Controller
  21. public class InterfaceErrorController implements ErrorController {
  22.     private static final String ERROR_PATH="/error";
  23.     private ErrorAttributes errorAttributes;
  24.     @Override
  25.     public String getErrorPath() {
  26.         return ERROR_PATH;
  27.     }
  28.     @Autowired
  29.     public InterfaceErrorController(ErrorAttributes errorAttributes) {
  30.         this.errorAttributes=errorAttributes;
  31.     }
  32.     /**
  33.      * web页面错误处理
  34.      */
  35.     @RequestMapping(value=ERROR_PATH,produces="text/html")
  36.     @ResponseBody
  37.     public String errorPageHandler(HttpServletRequest request,HttpServletResponse response) {
  38.         ServletWebRequest requestAttributes =  new ServletWebRequest(request);
  39.         Map<String, Object> attr = this.errorAttributes.getErrorAttributes(requestAttributes, false);
  40.         JSONObject jsonObject = new JSONObject();
  41.         ErrorCode errorCode = new ErrorCode(false, (int) attr.get("status"), (String) attr.get("message"));
  42.         return JSONObject.toJSONString(errorCode);
  43.     }
  44.     /**
  45.      * 除web页面外的错误处理,比如json/xml等
  46.      */
  47.     @RequestMapping(value=ERROR_PATH)
  48.     @ResponseBody
  49.     public ResultCode errorApiHander(HttpServletRequest request) {
  50.         ServletWebRequest requestAttributes = new ServletWebRequest(request);
  51.         Map<String, Object> attr=this.errorAttributes.getErrorAttributes(requestAttributes, false);
  52.         return new ErrorCode(false, (int)attr.get("status"), (String) attr.get("message"));
  53.     }
  54. }
  • 当是浏览器访问时返回json字符串

    image

  • 当是其他请求时返回自定义的ErrorCode

    image

  • ErrorCode代码如下
  1. import lombok.AllArgsConstructor;
  2. import lombok.Data;
  3. import lombok.ToString;
  4. @ToString
  5. @Data
  6. @AllArgsConstructor
  7. public class ErrorCode implements ResultCode{
  8.     private boolean success;
  9.     private int code;
  10.     private String message;
  11.     @Override
  12.     public boolean success() {
  13.         return false;
  14.     }
  15.     @Override
  16.     public int code() {
  17.         return 0;
  18.     }
  19.     @Override
  20.     public String message() {
  21.         return null;
  22.     }
  23. }
  • ResultCode代码如下
  1. public interface ResultCode {
  2.     //操作是否成功,true为成功,false操作失败
  3.     boolean success();
  4.     //操作代码
  5.     int code();
  6.     //提示信息
  7.     String message();
  8. }

@ControllerAdvice

  • 上面我们提到ErrorController可对全局错误进行处理,但是其获取不到异常的具体信息,同时也无法根据异常类型进行不同的响应,例如对自定义异常的处理
  • 而@ControllerAdvice可对全局异常进行捕获,包括自定义异常
  • 需要清楚的是,其是应用于对springmvc中的控制器抛出的异常进行处理,而对于404这样不会进入控制器处理的异常不起作用,所以此时还是要依靠ErrorController来处理

 

问题:

 

  • 实际上,当出现错误,如获取值为空或出现异常时,我们并不希望用户看到异常的具体信息,而是希望对对应的错误和异常做相应提示
  • 在MVC框架中很多时候会出现执行异常,那我们就需要加try/catch进行捕获,如果service层和controller层都加上,那就会造成代码冗余

 

解决方法:

 

  • 统一返回的数据格式,如上的ResultCode,可实现其做更多扩展,对于程序的可预知错误,我们采取抛出异常的方式,再统一处理
  • 我们在编程时的顺序是先校验判断,有问题则抛出异常信息,最后执行具体的业务操作,返回成功信息
  • 在统一异常处理类中去捕获异常,无需再代码中try/catch,向用户返回统一规范的响应信息

异常处理流程

系统对异常的处理使用统一的异常处理流程:

  1. 自定义异常类型
  2. 自定义错误代码及错误信息
  3. 对于可预知的异常由程序员在代码中主动抛出,有SpringMVC统一捕获
    可预知异常是程序员在代码中手动抛出本系统定义的特点异常类型,由于是程序员抛出的异常,通常异常信息比较齐全,程序员在抛出时会指定错误代码及错误信息,获取异常信息也比较方便
  4. 对于不可预知的异常(运行时异常)由SpringMVC统一捕获Exception类型的异常
    不可预知的异常通常是由于系统出现bug、或一些不可抗拒的错误(比如网络中断、服务器宕机等),异常类型为RuntimeException类型(运行时异常)
  5. 可预知异常及不可预知异常最终都会采用统一的信息格式(错误代码+错误信息)来表示,最终也会随请求响应给客户端

异常抛出及处理流程

image

  1. 在controller、service、dao中程序员抛出自定义异常;SpringMVC框架抛出框架异常类型
  2. 统一由异常捕获类捕获异常并进行处理
  3. 捕获到自定义异常则直接取出错误代码及错误信息,响应给用户
  4. 捕获到非自定义异常类型首先从Map中找该异常类型是否对应具体的错误代码,如果有则取出错误代码和错误信息并响应给用户,如果从Map中占不到异常类型所对应的错误代码则统一为99999错误代码并响应给用户
  5. 将错误代码及错误信息以json格式响应给用户

下面就开始我们的异常处理编程

一、可预知异常

  1. 自定义异常类
  1. import com.xuecheng.framework.model.response.ResultCode;
  2. import jdk.nashorn.internal.objects.annotations.Getter;
  3. /**
  4.  * @Author: jiangweifan
  5.  * @Date: 2019/3/4 20:06
  6.  * @Description:
  7.  */
  8. public class CustomException extends RuntimeException {
  9.     private ResultCode resultCode;
  10.     public CustomException(ResultCode resultCode) {
  11.         super("错误代码:" + resultCode.code()+" 错误信息:" + resultCode.message());
  12.         this.resultCode = resultCode;
  13.     }
  14.     public ResultCode getResultCode() {
  15.         return resultCode;
  16.     }
  17. }
  1. 自定义异常抛出类
  1. import com.xuecheng.framework.model.response.ResultCode;
  2. /**
  3.  * @Author: jiangweifan
  4.  * @Date: 2019/3/4 20:09
  5.  * @Description:
  6.  */
  7. public class ExceptionCast {
  8.     public static void cast(ResultCode resultCode, boolean condition) {
  9.         if (condition) {
  10.             throw new CustomException(resultCode);
  11.         }
  12.     }
  13. }
  1. 异常捕获类
    使用@ControllerAdvice和@ExceptionHandler注解来捕获指定类型的异常
  1. import com.google.common.collect.ImmutableMap;
  2. import com.xuecheng.framework.model.response.CommonCode;
  3. import com.xuecheng.framework.model.response.ResponseResult;
  4. import com.xuecheng.framework.model.response.ResultCode;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.web.bind.annotation.ControllerAdvice;
  7. import org.springframework.web.bind.annotation.ExceptionHandler;
  8. import org.springframework.web.bind.annotation.ResponseBody;
  9. import java.net.SocketTimeoutException;
  10. /**
  11.  * @Author: jiangweifan
  12.  * @Date: 2019/3/4 20:13
  13.  * @Description:
  14.  */
  15. @ControllerAdvice
  16. @Slf4j
  17. public class ExceptionCatch {
  18.     @ExceptionHandler(CustomException.class)
  19.     @ResponseBody
  20.     public ResponseResult customException(CustomException e) {
  21.         log.error("catch exception : {} \r\nexception", e.getMessage(), e);
  22.         ResponseResult responseResult = new ResponseResult(e.getResultCode());
  23.         return responseResult;
  24.     }
  25. }

4.1 定义响应数据格式

  1. import lombok.Data;
  2. import lombok.NoArgsConstructor;
  3. import lombok.ToString;
  4. /**
  5.  * @Author: mrt.
  6.  * @Description:
  7.  * @Date:Created in 2018/1/24 18:33.
  8.  * @Modified By:
  9.  */
  10. @Data
  11. @ToString
  12. @NoArgsConstructor
  13. public class ResponseResult implements Response {
  14.     //操作是否成功
  15.     boolean success = SUCCESS;
  16.     //操作代码
  17.     int code = SUCCESS_CODE;
  18.     //提示信息
  19.     String message;
  20.     public ResponseResult(ResultCode resultCode){
  21.         this.success = resultCode.success();
  22.         this.code = resultCode.code();
  23.         this.message = resultCode.message();
  24.     }
  25.     public static ResponseResult SUCCESS(){
  26.         return new ResponseResult(CommonCode.SUCCESS);
  27.     }
  28.     public static ResponseResult FAIL(){
  29.         return new ResponseResult(CommonCode.FAIL);
  30.     }
  31. }
  32. 其中Response代码如下
  33. public interface Response {
  34.     public static final boolean SUCCESS = true;
  35.     public static final int SUCCESS_CODE = 10000;
  36. }

4.2 定义错误代码(ResultCode上文已给出)

  1. import com.xuecheng.framework.model.response.ResultCode;
  2. import lombok.ToString;
  3. /**
  4.  * Created by mrt on 2018/3/5.
  5.  */
  6. @ToString
  7. public enum CmsCode implements ResultCode {
  8.     CMS_ADDPAGE_EXISTSNAME(false,24001,"页面名称已存在!"),
  9.     CMS_GENERATEHTML_DATAURLISNULL(false,24002,"从页面信息中找不到获取数据的url!"),
  10.     CMS_GENERATEHTML_DATAISNULL(false,24003,"根据页面的数据url获取不到数据!"),
  11.     CMS_GENERATEHTML_TEMPLATEISNULL(false,24004,"页面模板为空!"),
  12.     CMS_GENERATEHTML_HTMLISNULL(false,24005,"生成的静态html为空!"),
  13.     CMS_GENERATEHTML_SAVEHTMLERROR(false,24005,"保存静态html出错!"),
  14.     CMS_COURSE_PERVIEWISNULL(false,24007,"预览页面为空!"),
  15.     CMS_TEMPLATEFILE_ERROR(false,24008,"模板文件需要.ftl后缀!"),
  16.     CMS_TEMPLATEFILE_NULL(false,24009,"模板文件为空!"),
  17.     CMS_TEMPLATEFILE_EXCEPTION(false,24010,"解析模板文件异常!"),
  18.     CMS_TEMPLATEFILE_FAIL(false,24011,"模板文件存储失败!"),
  19.     CMS_TEMPLATEFILE_DELETE_ERROR(false,24012,"模板文件删除失败!"),
  20.     CMS_Config_NOTEXISTS(false,24013,"不存在该数据模型!"),
  21.     CMS_PAGE_NULL(false,24014,"不存在该页面数据!"),
  22.     CMS_GENERATEHTML_CONTENT_FAIL(false,24014,"获取页面模板失败!");
  23.     //操作代码
  24.     boolean success;
  25.     //操作代码
  26.     int code;
  27.     //提示信息
  28.     String message;
  29.     private CmsCode(boolean success, int code, String message){
  30.         this.success = success;
  31.         this.code = code;
  32.         this.message = message;
  33.     }
  34.     @Override
  35.     public boolean success() {
  36.         return success;
  37.     }
  38.     @Override
  39.     public int code() {
  40.         return code;
  41.     }
  42.     @Override
  43.     public String message() {
  44.         return message;
  45.     }
  46. }
  1. 在方法中抛出异常进行测试
  1. @GetMapping("/list/{page}/{size}")
  2. public QueryResponseResult findList(@PathVariable("page") int page, @PathVariable("size")int size, QueryPageRequest queryPageRequest) {
  3.     ExceptionCast.cast(CmsCode.CMS_COURSE_PERVIEWISNULL, queryPageRequest == null);
  4.     return pageService.findList(page,size,queryPageRequest);
  5. }

最终方法得到以下结果
 

image

二、不可预知异常处理

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

浏览器访问结果如下:
 

image

至此我们完成了对全局异常的处理

欢迎关注公众号,后续文章更新通知,一起讨论技术问题 。

(0)

相关推荐