spring boot学习系列之统一异常处理6

在写应用程序的时候遇到异常,我们自然而然会用try catch捕获异常处理,但这样到处捕获比较繁琐,代码也比较冗余,直接抛出异常又不大友好。这时就可以做个通用的异常处理。统一大部分的异常,也可以比较专注于业务。我们可以做个控制层切面,下层不断往上层抛,控制层上统一异常处理。

我们可以用@ControllerAdvice@ExceptionHandler注解实现异常处理,应用抛出异常的时候跳转到一个友好的提示页面,以此规避页面上打印出大量异常堆栈信息,影响体验不说,还有可能被有心的骇客用来分析系统漏洞。如果前端Ajax异步化,也可以统一异常信息,返回json等格式的数据。@ControllerAdvice注解捕获控制层的异常,所有对于DAO、Service层不想吃掉的异常都要往上抛。@ExceptionHandler定义方法处理的异常类型,最后将Exception对象和请求URL映射到相应的异常界面中,这适应Spring Mvc控制层那一套,如果是异步,加个@ResponseBody注解,返回异常对象。

定义一个全局异常处理类,捕获异常,将操作用户信息和异常信息插入异常日志表。

异常处理类

  1. /**
  2.  * 统一异常处理
  3.  * 
  4.  * @version V5.0
  5.  * @author wenqy
  6.  */
  7. @ControllerAdvice
  8. public class GlobalExceptionHandler {
  9.     private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  10.     @Autowired
  11.     ExceptionLogExecutors executors;
  12.     /**
  13.      * 403 异常
  14.      */
  15. //  @ExceptionHandler({ UnauthorizedException.class })
  16. //  public String unauthorized(UnauthorizedException ue) {
  17. //      return “error/403”;
  18. //  }
  19.     /**
  20.      * 405 异常
  21.      * @param ue
  22.      * @return
  23.      * @author wenqy
  24.      */
  25.     @ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
  26.     public String notSupportedException(HttpRequestMethodNotSupportedException ue) {
  27.         return “error/405”;
  28.     }
  29.     @ExceptionHandler({ MyBusinessException.class })
  30.     public String serviceException(MyBusinessException se, Model model) {
  31.         logger.error(“业务异常:” + se.getMessage(), se);
  32.         model.addAttribute(“message”, se.getMessage());
  33.         return “error/businessception”;
  34.     }
  35.     /**
  36.      * 服务器异常
  37.      * @param throwable
  38.      * @param model
  39.      * @return
  40.      * @author wenqy
  41.      */
  42.     @ExceptionHandler({ Error.class, Exception.class, RuntimeException.class, Throwable.class })
  43.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  44.     public String globalException(Throwable throwable, Model model) {
  45.         String clientIp = WebUtils.getClientIp();
  46.         StringBuffer traceString = new StringBuffer(“ip:”).append(clientIp);
  47.         String stackTraceAsString = ExceptionUtils.getStackTraceAsString(throwable);
  48.         traceString.append(“exection:”).append(stackTraceAsString);
  49.         logger.error(traceString.toString());
  50.         ExceptionLog exceptionLog = new ExceptionLog();
  51.         exceptionLog.setUserId(IdGenerator.randomLong());
  52.         exceptionLog.setHostIp(clientIp);
  53.         exceptionLog.setCreatedDate(new Date());
  54.         exceptionLog.setExptTime(new Date());
  55.         exceptionLog.setUuid(IdGenerator.randomString(11));
  56.         exceptionLog.setStatckTrace(stackTraceAsString);
  57. // 扔到线程池执行
  58.         this.executors.execute(new ExceptionLogThread(exceptionLog));
  59. // 抛出异常码
  60.         model.addAttribute(“errCode”, exceptionLog.getUuid().substring(0,3)
  61.                 + ” “ + exceptionLog.getUuid().substring(3,7)
  62.                 + ” “ + exceptionLog.getUuid().substring(7,11));
  63.         return “error/500”;
  64.     }
  65. }

自定义异常

我们可以自定义些业务异常,用短语标识。

  1. /**
  2.  * 自定义业务异常
  3.  * 
  4.  * @version V5.0
  5.  * @author wenqy
  6.  */
  7. @SuppressWarnings(“serial”)
  8. public class MyBusinessException extends RuntimeException {
  9.     private String errCode;
  10.     private String message;
  11.     private String detail;
  12.     public MyBusinessException() {
  13.         super();
  14.     }
  15.     public MyBusinessException(String message) {
  16.         this(null,message,null);
  17.     }
  18.     public MyBusinessException(String errorCode, String message) {
  19.         this(errorCode,message,null);
  20.     }
  21.     public MyBusinessException(String errorCode, String message, String detail) {
  22.         super();
  23.         this.errCode = errorCode;
  24.         this.message = message;
  25.         this.detail = detail;
  26.     }
  27.     public String getErrCode() {
  28.         return errCode;
  29.     }
  30.     public void setErrCode(String errCode) {
  31.         this.errCode = errCode;
  32.     }
  33.     public String getMessage() {
  34.         return message;
  35.     }
  36.     public void setMessage(String message) {
  37.         this.message = message;
  38.     }
  39.     public String getDetail() {
  40.         return detail;
  41.     }
  42.     public void setDetail(String detail) {
  43.         this.detail = detail;
  44.     }
  45. }

异常日志

异常日志的保存应该是异步的,用工作线程处理。

  1. /**
  2.  * 
  3.  * 异常日志保存线程
  4.  * @version V5.0
  5.  * @author wenqy
  6.  */
  7. public class ExceptionLogThread implements Runnable {
  8.     private ExceptionLog exceptionLog;
  9.     public ExceptionLogThread(ExceptionLog log) {
  10.         this.exceptionLog = log;
  11.     }
  12.     @Override
  13.     public void run() {
  14. // 加载异常日志处理服务
  15.         ExceptionLogService logService = (ExceptionLogService) SpringAppContextHolder
  16.                 .getBean(ExceptionLogService.class);
  17.         logService.saveExceptionLog(exceptionLog);
  18.     }
  19. }

避免线程的开销太大,定义线程池。

  1. /**
  2.  * 异常日志处理线程池
  3.  * 
  4.  * @version V5.0
  5.  * @author wenqy
  6.  */
  7. @SuppressWarnings(“serial”)
  8. @Component
  9. public class ExceptionLogExecutors extends ThreadPoolTaskExecutor {
  10.     public ExceptionLogExecutors() {
  11.         super.setCorePoolSize(10);
  12.         super.setMaxPoolSize(20);
  13.     }
  14. }

定义Spring 应用上下文持有者,以便注入bean。

  1. /**
  2.  * 
  3.  * Spring ApplicationContext 持有者
  4.  * @version V5.0
  5.  * @author wenqy
  6.  */
  7. @Component
  8. public class SpringAppContextHolder implements ApplicationContextAware {
  9.     /**
  10.      * Spring 自动注入
  11.      */
  12.     private static ApplicationContext applicationContext = null;
  13.     @Override
  14.     public void setApplicationContext(ApplicationContext context) throws BeansException {
  15.         if (applicationContext == null) {
  16.             applicationContext = context;
  17.         }
  18.     }
  19.     public static ApplicationContext getApplicationContext() {
  20.         return applicationContext;
  21.     }
  22.     /**
  23.      * getBeanByName
  24.      * @param name
  25.      * @return
  26.      * @author wenqy
  27.      */
  28.     public static Object getBean(String name){
  29.         return getApplicationContext().getBean(name);
  30.     }
  31.     /**
  32.      * getBeanByClazz
  33.      * @param clazz
  34.      * @return
  35.      * @author wenqy
  36.      */
  37.     public static <T> T getBean(Class<T> clazz){
  38.         return getApplicationContext().getBean(clazz);
  39.     }
  40.     public static <T> T getBean(String name,Class<T> clazz){
  41.         return getApplicationContext().getBean(name, clazz);
  42.     }
  43. }

分层处理

然后是分层的那一套。。。

异常日志VO

  1. /**
  2.  * 异常日志VO
  3.  * 
  4.  * @version V5.0
  5.  * @author wenqy
  6.  */
  7. @SuppressWarnings(“serial”)
  8. public class ExceptionLog implements Serializable {
  9.     private Long id;
  10.     private String uuid; // 系统异常码
  11.     private String hostIp; // ip
  12.     private Date exptTime; // 异常时间
  13.     private String statckTrace; // 异常栈
  14.     private Date createdDate;
  15.     private Long userId; // 操作用户Id
  16.     // 省略setter、getter方法
  17. }

Mybatis Mapper定义DAO操作

  1. /**
  2.  * 异常日志Mapper
  3.  * 
  4.  * @version V5.0
  5.  * @author wenqy
  6.  */
  7. @Repository
  8. public interface ExceptionLogMapper {
  9.     /**
  10.      * 保存日志日志
  11.      * @param exceptionLog
  12.      * @author wenqy
  13.      */
  14.     public void saveExceptionLog(@Param(“exceptionLog”) ExceptionLog exceptionLog);
  15. }

配置文件 ExceptionLogMapper.xml

  1. <!– namespace必须指向Dao接口 –>
  2. <mapper namespace=“com.wenqy.mapper.one.ExceptionLogMapper”>
  3.     <!– 保存日志 –>
  4.     <insert id=“saveExceptionLog”>
  5.         insert into exception_log(uuid,host_ip,expt_time,created_date,stacktrace,user_id)
  6.             values(#{exceptionLog.uuid},#{exceptionLog.hostIp},#{exceptionLog.exptTime},
  7.             #{exceptionLog.createdDate},#{exceptionLog.statckTrace},#{exceptionLog.userId})
  8.     </insert>
  9. </mapper>

业务逻辑层 Service

  1. /**
  2.  * 异常日志 Service
  3.  * 
  4.  * @version V5.0
  5.  * @author wenqy
  6.  * @date   2017年12月23日
  7.  */
  8. @Service
  9. public class ExceptionLogServiceImpl implements ExceptionLogService {
  10.     @Autowired
  11.     private ExceptionLogMapper exceptionLogMapper;
  12.     @Override
  13.     public void saveExceptionLog(ExceptionLog exceptionLog) {
  14.         exceptionLogMapper.saveExceptionLog(exceptionLog);
  15.     }
  16. }

定义服务器异常页面模板 500.ftl

  1. <#assign webRoot=request.contextPath />
  2. <!DOCTYPE html>
  3. <html lang=“en”>
  4.   <head>
  5.     <meta charset=“utf-8”>
  6.     <meta http-equiv=“X-UA-Compatible” content=“IE=edge”>
  7.     <meta name=“viewport” content=“width=device-width, initial-scale=1”>
  8.     <!– The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags –>
  9.     <meta name=“description” content=“”>
  10.     <meta name=“author” content=“”>
  11.     <link rel=“icon” href=“${webRoot}/static/favicon.ico”>
  12.     <title>Signin Template for Bootstrap</title>
  13.     <!– Bootstrap core CSS –>
  14.     <link href=“${webRoot}/static/css/bootstrap/bootstrap.min.css” rel=“stylesheet”>
  15.     <!– IE10 viewport hack for Surface/desktop Windows 8 bug –>
  16.     <link href=“${webRoot}/static/css/bootstrap/ie10-viewport-bug-workaround.css” rel=“stylesheet”>
  17.     <!– Custom styles for this template –>
  18.     <link href=“${webRoot}/static/css/custom/signin.css” rel=“stylesheet”>
  19.     <!– Just for debugging purposes. Don’t actually copy these 2 lines! –>
  20.     <!–[if lt IE 9]><script src=”../../assets/js/ie8-responsive-file-warning.js”></script><![endif]–>
  21.     <script src=“${webRoot}/static/js/bootstrap/ie-emulation-modes-warning.js”></script>
  22.     <!– HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries –>
  23.     <!–[if lt IE 9]>
  24.       <script src=“${webRoot}/static/js/bootstrap/html5shiv.min.js”></script>
  25.       <script src=“${webRoot}/static/js/bootstrap/respond.min.js”></script>
  26.     <![endif]–>
  27.   </head>
  28. <body>
  29.     <div class=“container”>
  30.         <div class=“panel panel-success”>
  31.             <div class=“panel-heading”>
  32.                 <input id=“backBtn” class=“btn btn-primary” onclick=“history.go(-1);” type=“button” value=“返回上一页” >
  33.             </div>
  34.             <div class=“panel-body”>
  35.                 <span>系统繁忙,请稍后重试,若仍有问题,请联系客服,电话:XXX-XXXX-XXXX</span>
  36.                 <span>请将状态码&nbsp;<strong>${ errCode }</strong>&nbsp;告知客服</span>
  37.             <br/>
  38.             </div>
  39.         </div>
  40.     </div>
  41. </body>
  42. </html>

业务异常的界面,常用HTTP异常状态相应的异常界面等等,不一一列举了。

还有一些异常工具类,ID生成器、Web工具类等等。。。

查看效果

发生业务异常测试。。。

business_exception.png

测试发生500异常

500_exception

插入异常日志,ip记录有问题。。。

exception_table

大致统一异常处理就这样,当然还有其他实现方式,但这种切面方式胜在侵入性低,解耦强。。。

网易云音乐就不插了,伤不起。。。

One thought on “spring boot学习系列之统一异常处理6

发表评论

电子邮件地址不会被公开。 必填项已用*标注

7 + 10 = ?