由于错误在所难免,异常处理已经成为开发工作中不可或缺的部分。
在web开发中,我们通常不希望用户看到一个写满StackTrace的错误页面;同时,我们希望出现错误或发生异常时,开发运维人员可以看到详细的错误信息,以便进行查错和DEBUG。
所以,在开发过程中,应重视异常处理。在进行业务逻辑开发之前,就应该定义好自己的异常处理流程。
1. 异常处理流程概述
异常处理的对象分为两类:
- 错误的请求:程序处理前就已经发生,如RequestMapping中不存在该请求的URL以及其他的一些HTTP错误。
- 程序中的错误:各种异常,包括Runtime异常。
Springboot会统一处理第1种错误,由ErrorController捕获并进行处理,根据请求的Accpet字段返回错误页面或json数据。这里贴出Springboot自己的BasicErrorController实现:
@Controllerpublic class BasicErrorController implements ErrorController { @Value("${error.path:/error}") private String errorPath; private final ErrorAttributes errorAttributes; public BasicErrorController(ErrorAttributes errorAttributes) { Assert.notNull(errorAttributes, "ErrorAttributes must not be null"); this.errorAttributes = errorAttributes; } public String getErrorPath() { return this.errorPath; } @RequestMapping( value = {"${error.path:/error}"}, produces = {"text/html"} ) public ModelAndView errorHtml(HttpServletRequest request) { return new ModelAndView("error", this.getErrorAttributes(request, false)); } @RequestMapping({"${error.path:/error}"}) @ResponseBody public ResponseEntity> error(HttpServletRequest request) { Map body = this.getErrorAttributes(request, this.getTraceParameter(request)); HttpStatus status = this.getStatus(request); return new ResponseEntity(body, status); } //……}
也可以自己写一个ErrorController返回自定义页面。
对于程序中发送的异常,可以手动进行捕获。如果没有手动捕获或有所遗漏,Springboot提供了@ExceptionHandler(value={})对某种类型的异常统一进行处理。通常,可以通过转发(forward)或重定向(redirect)方式将该异常转给自己定制的ExceptionController进行处理。ExceptionController可以根据请求类型返回错误页面或json数据。
2. 自定义RuntimeException
可以定制自己的Exception进行异常信息记录:
public abstract class BaseRuntimeException extends RuntimeException { private static final long serialVersionUID = -1842796916322056555L; public BaseRuntimeException() { super(); } public BaseRuntimeException(String message) { super(message); } public BaseRuntimeException(String message, Throwable cause) { super(message, cause); } /** * 返回异常的错误码 */ public abstract Integer getErrorCode(); /** * 返回异常的描述(不带应用前缀) */ public abstract String getErrorMessage(); /** * 返回异常的日志(带应用前缀) */ public abstract String getErrorLog();}
然后再Service层定义该服务模块异常信息。
public class MiscServiceException extends BaseRuntimeException implements IReThrowException { private static final long serialVersionUID = -1844670008823631700L; private MiscErrorCode miscErrorCode = MiscErrorCode.SUCCESS; private String errorLog = MiscErrorCode.SUCCESS.getDesc(); public MiscServiceException() { super(); } public MiscServiceException(String message, Throwable cause) { super(message, cause); this.errorLog = message; } public MiscServiceException(String message) { super(message); this.errorLog = message; } public MiscServiceException(String message, MiscErrorCode miscErrorCode) { super(message); this.miscErrorCode = miscErrorCode; this.errorLog = message; } /** * 返回异常的错误码.
* 方便日志查看追踪. * * @see BaseRuntimeException#getErrorCode() */ @Override public Integer getErrorCode() { return miscErrorCode.getValue(); } /** * 返回异常的描述.
* 可直接用于前端错误提示. * * @see BaseRuntimeException#getErrorMessage() */ @Override public String getErrorMessage() { return miscErrorCode.getDesc(); } /** * 返回异常的日志.
* 用于服务器日志打印. * * @see BaseRuntimeException#getErrorLog() */ @Override public String getErrorLog() { return errorLog; }}
在Service层生成异常对象并抛出。
if (…) throw new MiscServiceException(“log message…”, MiscErrorCode.XXXERRIR);
在Manager层继续向上抛出。
public interface SomeService { DTO someFunc() throws MiscServiceException { //…}
在Controller层捕获异常,进行处理——返回相关页面。
try {//…} catch (MiscServiceException e) {log.error(e.getErrorLog());return ResponseView.fail(e.getErrorCode(), e.getErrorMessage());}
如此以来,代码中定义的异常和错误均可以捕捉。
由于BaseRuntimeException是一种RuntimeException,Mananger层声明方法是不加throws Exception也可以通过编译。小猿建议每一个Manager的方法都加上throws Exception声明。另外,BaseRuntimeException实际上也可以直接继承Exception,这样编译器会强制要求对其进行异常进行处理。
3. @ExceptionHandler
上述方案解决了一部分自定义异常。对于其他的自己未定义的Runtime Exception,例如Null Pointer Exception,Springboot提供了ExceptionHandler,用于捕获所有代码中没有主动catch的异常。通常,我们该异常转发(forward)或重定向(redirect)至某个自定义的Controller进行处理。
转发和重定向的效果不同:在浏览器端,转发的请求,页面不会跳转,URL不会改变;重定向的请求,URL会改变。重定向实际上是浏览器进行了两次请求。对于参数传递,两种方式采取的方法也不同。重定向方式可以使用RedirectAttributes传递参数,转发方式则将参数放置在Request的Attributes中。
转发:
@ControllerAdvicepublic class CustomExceptionHandler { /** * 将异常页转发到错误页 */ @ExceptionHandler(Exception.class) public ModelAndView handleError(HttpServletRequest req, Exception ex) { log.info("Exception e : " + ex, ex); if (BaseRuntimeException.class.isAssignableFrom(ex.getClass())) { BaseRuntimeException e = (BaseRuntimeException) ex; req.setAttribute("code", e.getErrorCode()); req.setAttribute("message", e.getErrorMessage()); } else { req.setAttribute("code", FrontCode.OTHER_ERROR.getCode()); req.setAttribute("message", FrontCode.OTHER_ERROR.getDesc()); } return new ModelAndView("forward:/exception"); }}
重定向:
/** * 将异常页使用重定向方式到错误页 * * @param req * @param ex * @param mode * @return */@ExceptionHandler(Exception.class) public ModelAndView handleError(HttpServletRequest req, Exception ex ,RedirectAttributes mode) { log.info("Exception e : " + ex,ex); ModelAndView mav = new ModelAndView(); if (BaseRuntimeException.class.isAssignableFrom(ex.getClass())) { BaseRuntimeException e = (BaseRuntimeException) ex; mode.addAttribute("code",e.getErrorCode()); mode.addAttribute("message",e.getErrorMessage()); } else { mode.addAttribute("code", FrontCode.OTHER_ERROR.getCode()); mode.addAttribute("message", FrontCode.OTHER_ERROR.getDesc()); } return new ModelAndView("redirect:/exception");}
4. ErrorController
在下文贴出的示例中,我们将异常处理的Controller也放在ErrorController中。其中,errorHtml方法同于对没有经过Controller层的错误进行处理,返回自定义错误页;exception和exceptionHtml方法负责接收ExceptionHandler转发或重定向的异常处理流,根据produces的类型是”json/application”还是“text/html”,分别返回json和错误页面。
@Controllerpublic class CommonErrorController implements ErrorController { @Autowired private UserSecurityHelper userSecurityHelper; private static final String ERROR_PATH = "error"; private static final String EXCEPTION_PATH = "exception"; @RequestMapping(value = ERROR_PATH) public ModelAndView errorHtml(HttpServletRequest request) { ModelAndView modelAndView = new ModelAndView("/error/error"); Object statusCode = request.getAttribute("javax.servlet.error.status_code"); //当请求的错误类型非404、403、402、401时,返回500的错误页面 if (statusCode == null || (!statusCode.equals(HttpStatus.NOT_FOUND.value()) && !statusCode.equals(HttpStatus.UNAUTHORIZED.value()) && !statusCode.equals(HttpStatus.PAYMENT_REQUIRED.value()) && !statusCode .equals(HttpStatus.FORBIDDEN.value()))) { statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); } modelAndView.addObject("code", statusCode); modelAndView.addObject("message", "你很神,找到了不存在的页面。"); return modelAndView; } /* * 使用forward转发. */ @RequestMapping(value = EXCEPTION_PATH, produces = "application/json") @ResponseBody public ResponseEntity exception(HttpServletRequest request) { Integer code = (Integer) request.getAttribute("code"); String message = (String) request.getAttribute("message"); return ResponseView.fail(code, message); } @RequestMapping(value = EXCEPTION_PATH, produces = {"text/html"}) public ModelAndView exceptionHtml(HttpServletRequest request) { EduWebUser eduWebUser = userSecurityHelper.getEduWebUser(); ModelAndView mav = new ModelAndView("/error/error"); mav.addObject("code", (Integer) request.getAttribute("code")); mav.addObject("message", (String) request.getAttribute("message")); mav.addObject("webUser", eduWebUser); return mav; } @Override public String getErrorPath() { return ERROR_PATH; }}
如果使用Redirect跳转,ErrorController中接收参数的相应代码要随之改变。
/*使用Redirect跳转*/@RequestMapping(value = EXCEPTION_PATH, produces = "application/json")@ResponseBodypublic ResponseEntity exception(HttpServletRequest request , @RequestParam(required = false) String message, @RequestParam(required = false) Integer code) { Map body = new HashMap<>(4); body.put("code", code); body.put("message", message); return new ResponseEntity(body, HttpStatus.OK);}@RequestMapping(value = EXCEPTION_PATH, produces = { "text/html"})public ModelAndView exceptionHtml(HttpServletRequest request , @RequestParam(required = false) String message, @RequestParam(required = false) Integer code) { ModelAndView mav = new ModelAndView("/error/error"); EduWebUser eduWebUser = userSecurityHelper.getEduWebUser(); mav.addObject("code", code); mav.addObject("message", message); mav.addObject("webUser", eduWebUser); return mav;}
5. 测试
我们定义了专门用于测试的Service和Controller。其中,throw测试程序中代码捕获异常,silent测试由ExceptionHandler捕获的异常。
public interface ExceptionService { public void testThrowException() throws MiscServiceException; public void testSilentException();}@Service("exceptionService")public class ExceptionServiceImpl implements ExceptionService { @Override public void testThrowException() throws MiscServiceException { throw new ForumException("Log Message"); } @Override public void testSilentException() { throw new ForumException("Log Message"); }}@RestController@RequestMapping("/exception")public class ExceptionController { @Resource private ExceptionService exceptionService; @RequestMapping("/throw") public ResponseEntity testThrow() { try { exceptionService.testThrowException(); return ResponseView.success(null); } catch (MiscServiceException e) { e.printStackTrace(); return ResponseView.fail(e.getErrorCode(), e.getErrorMessage()); } } @RequestMapping("/silent") public ResponseEntity testSilent() { exceptionService.testSilentException(); return ResponseView.success(null); }}
测试记录如下。
代码主动捕获异常:
Springboot通过forward方式统一捕获异常:
Springboot通过redirect方式统一捕获异常:
Springboot统一捕获异常,返回json:
对于Controller之前发生的错误和异常,返回自定义页面:
如果对我的更新内容很感兴趣,希望可以得到您的一个点赞和关注,这将是对我最大的鼓励和支持,感谢~不论多与少,我都会坚持更新。另外,私聊我【Java学习资料】可以收获互联网大厂Java面经和Java最新最全资料~帮助你早日拿到互联网大厂的offer~