背景
排查线上问题,只能通过日志,那么如何才能快速定位到问题,这个是需要好好设计的。如果设计得当,可以快速通过日志定位是自己写的代码问题,还是第三方问题,还是别人调用你的接口入参问题。
那么如何来设计异常,才能快速定位到问题呢?
如何设计
出发点:
1、良好的异常信息展示,出现问题可以快速找出原因;
2、被调用时明确告诉调用方是参数不合法还是系统内部出现问题;
3、业务异常友好的提示用户;
实现
异常进行分类
对异常进行分类,一般分为系统异常、业务异常。
1、系统异常包括资源环境异常、第三方异常、代码bug、数据异常、配置异常、参数不合法;
2、业务异常包括操作错误、条件不满足;
捕获异常并抛出
1)入参合法性验证
对外提供的接口都需要验证。
service方法的入参都需要验证。
2)响应结果验证
对第三方的接口返回都需要进行验证,不要等到用的时候才发现数据不正确或者格式不对。
3)业务前置条件验证
如你要买东西,那么需要验证你的余额够不够;你要看视频,需要验证你有没有观看权限等等;这些验证不通过都是需要抛出业务异常的。
验证原则:
1、越早验证越好,越有利于定位问题。
2、过度验证影响性能,需要综合考虑。
统一拦截
确保不会有异常堆栈展示给用户,保证展示给用户的信息是友好的。
web响应
以springmvc为例:
统一异常处理类
@ControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handleExp(HttpServletRequest request, Exception ex) {
//记录异常日志
if (ex instanceof WarnException || ex instanceof MethodArgumentNotValidException) {
//验证类异常 不报警
logger.warn("warning occurs:", ex);
} else {
logger.error("error occurs:", ex);
}
R r = new R();
if (ex instanceof RRException) {
r.put("code", ((RRException) ex).getCode());
r.put("msg", ((RRException) ex).getMsg());
} else if (ex instanceof DuplicateKeyException) {
r = R.error("数据库中已存在该记录");
} else if (ex instanceof AuthorizationException) {
r = R.error("没有权限,请联系管理员授权");
} else if (ex instanceof SQLException) {
r = R.error("数据库异常,请联系管理员");
} else if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException validException = (MethodArgumentNotValidException) ex;
List<ObjectError> errors = validException.getBindingResult().getAllErrors();
StringBuilder sb = new StringBuilder();
for (ObjectError error : errors) {
sb.append(error.getDefaultMessage()).append("<br/>");
}
r = R.error(sb.toString());
} else {
r = R.error();
}
return r;
}
}
返回值封装
public class R extends HashMap, Object> implements Serializable {
private static final int DEFAULT_ERROR_CODE = 500;
private static final int DEFAULT_SUCCESS_CODE = 0;
private static final long serialVersionUID = 1572870614935320893L;
public R() {
put("code", DEFAULT_SUCCESS_CODE);
}
public static R error() {
return error(DEFAULT_ERROR_CODE, "程序异常,请联系管理员");
}
public static R error(String msg) {
return error(DEFAULT_ERROR_CODE, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R error(String code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("code", DEFAULT_SUCCESS_CODE);
r.put("msg", msg);
return r;
}
public static R ok(Map, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
@Override
public R put(String key, Object value) {
super.put(key, value);
return this; }
public R put(Object value) {
super.put("data", value);
return this; }
}
xml配置
<bean class="com.xxx.utils.GlobalExceptionHandler"/>
RPC服务
在提供者,定义异常拦截器
@Activate(group = Constants.PROVIDER)
public class ExceptionFilter implements Filter {
private final Logger logger;
public ExceptionFilter() {
this(LoggerFactory.getLogger(ExceptionFilter.class));
}
public ExceptionFilter(Logger logger) {
this.logger = logger;
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Result result = invoker.invoke(invocation);
if (result.hasException()) {
Throwable throwable = result.getException();
if (throwable instanceof RRException) {
RRException ex = (RRException) throwable;
return new RpcResult(ex);
}
}
return result;
}
}
在META-INF.dubbo下的com.alibaba.dubbo.rpc.Filter文件里面定义
exceptionFilter=com.xxx.solar.ExceptionFilter
异常设计
通过包名来区分是业务异常还是系统异常:
业务异常包名:com.xxx.xxx.exceptions.biz
系统异常包名:com.xxx.xxx.exceptions.system
RRException输出error日志
WarnException输出warn日志
如果需要报警,那么可以实现RRException异常类
如果不需要进行报警,那么可以实现WarnException异常类
错误类的定义建议使用枚举,不要定义在常量类中定义code。
PS:ELK只会对error occurs:进行拦截。
注意:本文归作者所有,未经作者允许,不得转载