前言
本篇文章主要介绍的是SpringBoot非web项目进行全局异常的处理。
SpringBoot版本:2.1.9.RELEASE
Mybatis Plus版本:3.3.0
上个项目使用的是SpringBoot+Mybatis Plus+zbus,项目架构是:zbus分为客户端和服务端,两者通过RPC进行调用。
主要工作:通过Spring AOP处理、捕获异常,并将异常信息记录到日志中。
一. 先看我的pom文件,之所以将Spring换为SpringBoot就是看中了它的自动配置功能,能省略很多配置代码,简化开发:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
注意:此处没有引入web模块!
二.
- 新增异常枚举类和自定义异常类,继承RuntimeException。
package com.junya.util.exception;
/**
* 自定义全局异常类
*
* @author ZhangChao
* @date 2019/10/18 13:23
* @since 1.0.0
*/
public class GlobalException extends RuntimeException {
private static final long serialVersionUID = 6958499252468627021L;
/**
* 错误码
*/
private String code;
public GlobalException(String code, String msg) {
super(msg);
this.code = code;
}
public GlobalException(GlobalErrorCodeEnum errorCode) {
super(errorCode.getMsg());
this.code = errorCode.getCode();
}
public GlobalException(GlobalErrorCodeEnum errorCode, String msg){
super(errorCode.getMsg()+msg);
this.code = errorCode.getCode();
}
public GlobalException(String code, String msg, Throwable throwable){
super(msg,throwable);
this.code = code;
}
public GlobalException(GlobalErrorCodeEnum errorCode, Throwable throwable){
super(errorCode.getMsg(),throwable);
this.code = errorCode.getCode();
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
package com.yorma.enterprise.common.exception;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 自定义全局异常枚举
*
* @author ZhangChao
* @date 2019/10/18 13:15
* @since 1.0.0
*/
public enum GlobalErrorCode {
/** 未知异常 */
UNKNOWN_EXCEPTION("CSEP001","未知异常,请联系管理员"),
/** 系统错误 */
SYSTEM_ERROR("CSEP002","系统错误"),
/** 类转换异常 */
CLASS_CAST_EXCEPTION("CSEP003","类型强制转换异常"),
/** 算术条件异常 */
ARITHMETIC_EXCEPTION("CSEP004","算术条件异常"),
/** 空指针异常 */
NULL_POINTER_EXCEPTION("CSEP005","空指针异常"),
/** 字符串转换为数字异常 */
NUMBER_FORMAT_EXCEPTION("CSEP006","字符串转换为数字异常"),
/** 数组下标越界异常 */
ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION("CSEP007","数组下标越界异常"),
/** 方法未找到异常 */
NO_SUCH_METHOD_EXCEPTION("CSEP008","方法未找到异常"),
/** 未找到类定义错误 */
NO_CLASS_DEF_FOUND_ERROR("CSEP009","未找到类定义错误"),
/** 未找到类定义错误 */
CLASS_NOT_FOUND_EXCEPTION("CSEP010","找不到类异常"),
/** 索引越界异常 */
INDEX_OUT_OF_BOUNDS_EXCEPTION("CSEP011","索引越界异常")
;
private String code;
private String msg;
GlobalErrorCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public static void main(String[] args) {
String[] arr = {"IndexOutOfBoundsException"};
List<String> resultList = new ArrayList<>(arr.length);
Collections.addAll(resultList,arr);
resultList.forEach(item->{
System.out.println(item.toUpperCase());
});
}
}
- 新增处理异常Handler 类
package com.yorma.enterprise.zbus.provider.config;
import com.yorma.enterprise.common.exception.ExceptionUtil;
import com.yorma.enterprise.common.result.ResponseMessage;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 全局异常拦截处理
*
* --注意:不加order注解会导致事务不回滚!!
*
* @author ZhangChao
* @date 2019/9/12 15:30
* @since 1.0.0
*/
@Component
@Aspect
@Order(1)
public class GlobalExceptionHandler {
private static Logger LOGGER;
/**
* 切点,找到那些方面要切
*/
// #排除掉basic包# @Pointcut("execution(public * com.yorma.enterprise.service.impl..*.*(..)) && !execution(public * com.yorma.enterprise.service.impl.basic.*.*(..))")
@Pointcut("execution(public * com.yorma.enterprise.service.impl..*.*(..)) && !execution(public * com.yorma.enterprise.service.impl.business.*.*(..))\"")
public void exception(){}
/**
* 环切,执行方法前后都切
* @param joinPoint
* @return
*/
@Around("exception()")
public ResponseMessage ExceptionHandler(ProceedingJoinPoint joinPoint){
ResponseMessage result = null;
long startTime = System.currentTimeMillis();
//获取目标Class
Class targetClass = joinPoint.getTarget().getClass();
//目标Class的Logger
LOGGER = LoggerFactory.getLogger(targetClass);
//获取目标类名称
String clazzName = joinPoint.getTarget().getClass().getName();
//获取目标类方法名称
String methodName = joinPoint.getSignature().getName();
try {
LOGGER.info( "{}: {}: {}: start...", clazzName, methodName, joinPoint.getArgs());
// 调用目标方法
result= (ResponseMessage) joinPoint.proceed();
long endTime = System.currentTimeMillis() - startTime;
LOGGER.info( "{}: {}: end... cost time: {} ms", clazzName, methodName, endTime);
return result;
}catch (Throwable e){
e.printStackTrace();
//异常堆栈信息输出到logger日志中
LOGGER.info(clazzName+"."+methodName+"方法执行出现异常! => "+ e.toString());
ExceptionUtil.getFullStackTrace(e);
//使事务生效
// throw new RuntimeException(clazzName+"."+methodName+"方法执行出现异常! => "+ e.toString());
result = ExceptionUtil.ResExHandle(e);
return result;
}
}
}
- ExceptionUtil.getFullStackTrace(e)方法是为了将异常信息存到日志中,以方便后期排查,这里贴一下具体代码,两种方式参数分别是Exception和Throwable:
/**
* 异常堆栈信息保存到日志中
* @param ex
*/
public static void getFullStackTrace(Exception ex) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream pout = new PrintStream(out);
ex.printStackTrace(pout);
String ret = new String(out.toByteArray());
pout.close();
try {
out.close();
} catch (Exception e) {
}
ex.printStackTrace();
logger.error(ret);
}
/**
* 参数是Throwable
* @param e
* @return
*/
public static void getFullStackTrace(Throwable e){
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
try {
e.printStackTrace(pw);
pw.flush();
sw.flush();
logger.error(sw.toString());
} finally {
pw.close();
}
}
- ExceptionUtil.ResExHandle(e)方法是封装异常信息返前端的,代码如下:
/**
* 封装异常类
* @param e
* @return
*/
public static ResponseMessage ResExHandle(Throwable e){
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setSuccess(false);
String eStr = e.toString();
for (GlobalErrorCode global : GlobalErrorCode.values()){
if (eStr.toUpperCase().contains(global.toString().replaceAll("_",""))){
responseMessage.setStatus(global.getCode());
responseMessage.setMsg(global.getMsg());
break;
}
}
//如果是以前封装的那个YmException异常
if (eStr.contains("YmException")){
for (StatusCodeEnum statusCodeEnum : StatusCodeEnum.values()){
if (eStr.contains(statusCodeEnum.getMsg())){
if (eStr.contains("Data too long for column")){
String field = eStr.replaceAll("[\\s\\S]+Data too long for column '(.+)' at[\\s\\S]+","$1");
responseMessage.setStatus(statusCodeEnum.getCode());
responseMessage.setMsg(statusCodeEnum.getMsg()+",字段长度超过限制:"+field);
break;
}
if (eStr.contains("Unknown column")){
String field = eStr.replaceAll("[\\s\\S]+Unknown column '(.+)' in[\\s\\S]+","$1");
responseMessage.setStatus(statusCodeEnum.getCode());
responseMessage.setMsg(statusCodeEnum.getMsg()+",未知的字段:"+field);
break;
}
if (eStr.contains("doesn't have a default value")){
String field = eStr.replaceAll("[\\s\\S]+Field '(.+)' doesn't have a default value[\\s\\S]+","$1");
responseMessage.setStatus(statusCodeEnum.getCode());
responseMessage.setMsg(statusCodeEnum.getMsg()+",此字段必须有值:"+field);
break;
}
responseMessage.setStatus(statusCodeEnum.getCode());
responseMessage.setMsg(statusCodeEnum.getMsg());
break;
}
}
}
if ("".equals(responseMessage.getStatus()) || "".equals(responseMessage.getMsg())){
responseMessage.setStatus(GlobalErrorCode.UNKNOWN_EXCEPTION.getCode());
responseMessage.setMsg(GlobalErrorCode.UNKNOWN_EXCEPTION.getMsg());
}
return responseMessage;
}
封装了几种常见的异常信息,如:空指针、数据库字段不存在等。
总结
用aop的方式来处理全局异常还是比较简单的。
PS: 更多技术干货,欢迎大家来我的个人博客 yak33的技术人生