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

in Java with 0 comment

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

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

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

异常处理类

/**
 * 统一异常处理
 * 
 * @version V5.0
 * @author wenqy
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    @Autowired
    ExceptionLogExecutors executors;
    /**
     * 403 异常
     */
//  @ExceptionHandler({ UnauthorizedException.class })
//  public String unauthorized(UnauthorizedException ue) {
//      return “error/403”;
//  }
    /**
     * 405 异常
     * @param ue
     * @return
     * @author wenqy
     */
    @ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
    public String notSupportedException(HttpRequestMethodNotSupportedException ue) {
        return “error/405”;
    }
    @ExceptionHandler({ MyBusinessException.class })
    public String serviceException(MyBusinessException se, Model model) {
        logger.error(“业务异常:” + se.getMessage(), se);
        model.addAttribute(“message”, se.getMessage());
        return “error/businessception”;
    }
    /**
     * 服务器异常
     * @param throwable
     * @param model
     * @return
     * @author wenqy
     */
    @ExceptionHandler({ Error.class, Exception.class, RuntimeException.class, Throwable.class })
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String globalException(Throwable throwable, Model model) {
        String clientIp = WebUtils.getClientIp();
        StringBuffer traceString = new StringBuffer(“ip:”).append(clientIp);
        String stackTraceAsString = ExceptionUtils.getStackTraceAsString(throwable);
        traceString.append(“exection:”).append(stackTraceAsString);
        logger.error(traceString.toString());
        ExceptionLog exceptionLog = new ExceptionLog();
        exceptionLog.setUserId(IdGenerator.randomLong());
        exceptionLog.setHostIp(clientIp);
        exceptionLog.setCreatedDate(new Date());
        exceptionLog.setExptTime(new Date());
        exceptionLog.setUuid(IdGenerator.randomString(11));
        exceptionLog.setStatckTrace(stackTraceAsString);
// 扔到线程池执行
        this.executors.execute(new ExceptionLogThread(exceptionLog));
// 抛出异常码
        model.addAttribute(“errCode”, exceptionLog.getUuid().substring(0,3)
                + ” “ + exceptionLog.getUuid().substring(3,7)
                + ” “ + exceptionLog.getUuid().substring(7,11));
        return “error/500”;
    }
}

自定义异常

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

/**
 * 自定义业务异常
 * 
 * @version V5.0
 * @author wenqy
 */
@SuppressWarnings(“serial”)
public class MyBusinessException extends RuntimeException {
    private String errCode;
    private String message;
    private String detail;
    public MyBusinessException() {
        super();
    }
    public MyBusinessException(String message) {
        this(null,message,null);
    }
    public MyBusinessException(String errorCode, String message) {
        this(errorCode,message,null);
    }
    public MyBusinessException(String errorCode, String message, String detail) {
        super();
        this.errCode = errorCode;
        this.message = message;
        this.detail = detail;
    }
    public String getErrCode() {
        return errCode;
    }
    public void setErrCode(String errCode) {
        this.errCode = errCode;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public String getDetail() {
        return detail;
    }
    public void setDetail(String detail) {
        this.detail = detail;
    }
}

异常日志

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

/**
 * 
 * 异常日志保存线程
 * @version V5.0
 * @author wenqy
 */
public class ExceptionLogThread implements Runnable {
    private ExceptionLog exceptionLog;
    public ExceptionLogThread(ExceptionLog log) {
        this.exceptionLog = log;
    }
    @Override
    public void run() {
// 加载异常日志处理服务
        ExceptionLogService logService = (ExceptionLogService) SpringAppContextHolder
                .getBean(ExceptionLogService.class);
        logService.saveExceptionLog(exceptionLog);
    }
}

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

/**
 * 异常日志处理线程池
 * 
 * @version V5.0
 * @author wenqy
 */
@SuppressWarnings(“serial”)
@Component
public class ExceptionLogExecutors extends ThreadPoolTaskExecutor {
    public ExceptionLogExecutors() {
        super.setCorePoolSize(10);
        super.setMaxPoolSize(20);
    }
}

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

/**
 * 
 * Spring ApplicationContext 持有者
 * @version V5.0
 * @author wenqy
 */
@Component
public class SpringAppContextHolder implements ApplicationContextAware {
    /**
     * Spring 自动注入
     */
    private static ApplicationContext applicationContext = null;
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        if (applicationContext == null) {
            applicationContext = context;
        }
    }
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    /**
     * getBeanByName
     * @param name
     * @return
     * @author wenqy
     */
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }
    /**
     * getBeanByClazz
     * @param clazz
     * @return
     * @author wenqy
     */
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

分层处理

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

异常日志VO

/**
 * 异常日志VO
 * 
 * @version V5.0
 * @author wenqy
 */
@SuppressWarnings(“serial”)
public class ExceptionLog implements Serializable {
    private Long id;
    private String uuid; // 系统异常码
    private String hostIp; // ip
    private Date exptTime; // 异常时间
    private String statckTrace; // 异常栈
    private Date createdDate;
    private Long userId; // 操作用户Id
    // 省略setter、getter方法
}

Mybatis Mapper定义DAO操作

/**
 * 异常日志Mapper
 * 
 * @version V5.0
 * @author wenqy
 */
@Repository
public interface ExceptionLogMapper {
    /**
     * 保存日志日志
     * @param exceptionLog
     * @author wenqy
     */
    public void saveExceptionLog(@Param(“exceptionLog”) ExceptionLog exceptionLog);
}

配置文件 ExceptionLogMapper.xml

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

业务逻辑层 Service

/**
 * 异常日志 Service
 * 
 * @version V5.0
 * @author wenqy
 * @date   2017年12月23日
 */
@Service
public class ExceptionLogServiceImpl implements ExceptionLogService {
    @Autowired
    private ExceptionLogMapper exceptionLogMapper;
    @Override
    public void saveExceptionLog(ExceptionLog exceptionLog) {
        exceptionLogMapper.saveExceptionLog(exceptionLog);
    }
}

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

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

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

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

查看效果

发生业务异常测试。。。

business_exception

测试发生500异常

500_exception

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

exception_table

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

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