在写应用程序的时候遇到异常,我们自然而然会用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>请将状态码 <strong>${ errCode }</strong> 告知客服</span>
<br/>
</div>
</div>
</div>
</body>
</html>
业务异常的界面,常用HTTP异常状态相应的异常界面等等,不一一列举了。
还有一些异常工具类,ID生成器、Web工具类等等。。。
查看效果
发生业务异常测试。。。
测试发生500异常
插入异常日志,ip记录有问题。。。
大致统一异常处理就这样,当然还有其他实现方式,但这种切面方式胜在侵入性低,解耦强。。。
网易云音乐就不插了,伤不起。。。
本文由 wenqy 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Nov 8,2020