1.概述
日常开发的时候经常会去别人负责的工程里开发代码,如果各个工程的代码风格各异会影响日常开发效率,故需要进行一些通用设计来提升日常开发效率。
2.通用设计
2.1.异常
2.1.1.基础知识
异常是可以传递的,里层的异常可以往外层传递,比如下图代码(图2.1.1.1),最终会打印三个异常堆栈(图2.1.1.2)
图2.1.1.1
图2.1.1.2
看到过很多自定义异常不支持传递异常,当使用这些自定义异常时往往会先打印日志再抛出异常(图2.1.1.3),代码繁琐并且容易漏打日志
图2.1.1.3
2.1.2.自定义异常
自定义异常应包含的几个关键要素:
- 错误码(处理异常时需根据错误码打印不同级别的日志)
- 错误信息(给用户看的,比较简单明了)
- 描述信息(给技术人员看的,会携带排查问题所需的关键信息,比如用户查不到会携带用户号)
- 异常(可以将里层的异常不断地往外层传递,由最外层打印异常堆栈)
样例:
public class SsoException extends RuntimeException {
private String errorCode;
private String errorMsg;
private String message;
private String param;
public SsoException(ErrorCode errorCode) {
this(null, errorCode, null);
}
public SsoException(ErrorCode errorCode, String msg, Object... ss) {
this(null, errorCode, msg, ss);
}
public SsoException(Throwable e, ErrorCode errorCode) {
this(e, errorCode, null);
}
public SsoException(Throwable e, ErrorCode errorCode, String msg, Object... ss) {
super(e);
if (errorCode == null) {
errorCode = ErrorCode.UnknownError;
}
this.errorCode = errorCode.getRetCode();
this.errorMsg = errorCode.getRetMsg();
this.message = StringUtils.isBlank(msg) ? "[" + errorCode.getRetCode() + "][" + errorCode.getRetMsg() + "]" : MessageFormatter.arrayFormat(msg, ss).getMessage();
}
public SsoException(ErrorCode errorCode, ParamNameEnum paramName, String msg, Object... ss) {
if (errorCode == null) {
errorCode = ErrorCode.UnknownError;
}
this.errorCode = errorCode.getRetCode();
this.errorMsg = errorCode.getRetMsg();
this.message = StringUtils.isBlank(msg) ? "[" + errorCode.getRetCode() + "][" + errorCode.getRetMsg() + "]" : MessageFormatter.arrayFormat(msg, ss).getMessage();
this.param = paramName == null ? null : paramName.name();
}
@Override
public String getMessage() {
return message;
}
2.1.3.使用自定义异常
当捕获某处异常并且想中断执行,抛自定义异常并携带原始异常(图2.1.3.1)
图2.1.3.1
2.1.4.处理自定义异常
http接口统一由切面处理,dubbo接口统一由filter处理,日常开发时无需操心异常的处理,统一处理自定义异常时可以根据携带的错误码打印不同级别的日志,描述信息可根据监控系统规则打标(图2.1.4.1)
图2.1.4.1
2.2.打印请求和响应日志
不管是接口提供方还是接口调用方,看到过很多代码都是针对单个接口打印请求和响应日志(图2.2.1和图2.2.2),代码繁琐并且容易漏打
图2.2.1
图2.2.2
http可以使用切面,dubbo可以使用filter来统一打印请求和响应日志,日常开发时无需操心请求和响应日志的打印
http接口统一打印日志类:
@Aspect
@Component
@Slf4j
public class SystemLogAspect {
@Resource
private BizApplicationConfig bizApplicationConfig;
@Pointcut("@annotation(com.lianlianpay.cb.va.sso.aop.SystemLog)")
public void controllerAspect() {
}
@Around("controllerAspect()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String arg = null;
Object result = null;
String uri = null;
String requestIp = null;
String userId = null;
String bizSource = null;
String requestSource = null;
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
arg = JSON.toJSONString(joinPoint.getArgs());
uri = request.getRequestURI() + " " + request.getMethod();
requestIp = HttpUtils.getIp(request);
bizSource = request.getHeader(SsoConstants.BIZ_SOURCE_HEADER_TAG);
CookieUser cookieUser = (CookieUser) request.getAttribute("cookieUser");
userId = cookieUser == null ? null : cookieUser.getUserId();
requestSource = request.getHeader(SsoConstants.HEADER_REQUEST_SOURCE);
RequestSourceContext.set(requestSource);
BizSourceContext.set(bizSource);
ApContext.set(bizApplicationConfig.getAp(bizSource));
log.info("http请求开始:uri={},requestIp={},request={},userId={},bizSource={},requestSource={}",
uri, requestIp, arg, userId, bizSource, requestSource);
result = joinPoint.proceed();
} catch (Throwable e) {
result = ExceptionUtil.handleException(e, "http请求异常:uri=" + uri);
} finally {
RequestSourceContext.remove();
BizSourceContext.remove();
ApContext.remove();
log.info("http请求结束:uri={},requestIp={},request={},userId={},bizSource={},requestSource={},response={},cost={}ms",
uri, requestIp, arg, userId, bizSource, requestSource, JSON.toJSONString(result), System.currentTimeMillis() - startTime);
}
return result;
}
}
dubbo服务端统一打印日志类:
@Slf4j
public class ProviderLogFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long start = System.currentTimeMillis();
Result result = invoker.invoke(invocation);
//请求参数脱敏
List<Object> argList = null;
if (invocation.getArguments() != null) {
argList = Arrays.stream(invocation.getArguments()).map(e -> {
if (e instanceof String) {
return MaskUtil.mask((String) e);
}
return e;
}).collect(Collectors.toList());
}
//返回结果处理
if (result.hasException()) {
Throwable e = result.getException();
if (e instanceof SsoException) {
SsoException se = (SsoException) e;
//重置result
if (ErrorCode.ParamsError.getRetCode().equals(se.getErrorCode())) {
((RpcResult) result).setException(null);
((RpcResult) result).setValue(ResultRichUtil.format(se.getErrorCode(), se.getMessage()));
} else {
((RpcResult) result).setException(null);
((RpcResult) result).setValue(ResultRichUtil.format(se.getErrorCode(), se.getErrorMsg()));
}
//打印日志
//特殊错误码不做日志采集
BizApplicationConfig bizApplicationConfig = SpringContextUtil.getBean(BizApplicationConfig.class);
List<String> specialCodeList = bizApplicationConfig.getSpecialErrorCode();
if (specialCodeList.contains(se.getErrorCode())) {
log.info("[{}][{}]invoke fail:请求={},响应={},耗时={}ms",
invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start, e);
} else if (se.getErrorCode().equals(ErrorCode.UnknownError.getRetCode())) {
log.error("[{}][{}][{}]invoke fail:请求={},响应={},耗时={}ms",
LogTagConstant.SYSTEM_ERROR, invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start, e);
} else if (se.getErrorCode().equals(ErrorCode.RemoteInvokeFail.getRetCode())) {
log.warn("[{}][{}][{}]invoke fail:请求={},响应={},耗时={}ms",
LogTagConstant.REMOTE_ERROR, invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start, e);
} else {
log.warn("[{}][{}][{}]invoke fail:请求={},响应={},耗时={}ms",
LogTagConstant.REQUEST_ERROR, invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start, e);
}
} else if (e instanceof BaseException) {
BaseException be = (BaseException) e;
//重置Result
((RpcResult) result).setException(null);
((RpcResult) result).setValue(ResultRichUtil.format(be.getErrCode(), be.getErrMsg()));
//打印日志
log.error("[{}][{}]invoke fail:请求={},响应={},耗时={}ms",
invoker.getInterface(), invocation.getMethodName(), JSON.toJSONString(argList),
JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start, e);
} else {
//重置Result
((RpcResult) result).setException(null);
((RpcResult) result).setValue(ResultRichUtil.format(ErrorCode.UnknownError.getRetCode(), ErrorCode.UnknownError.getRetMsg()));
//打印日志
log.error("[{}][{}][{}]invoke fail:请求={},响应={},耗时={}ms",
LogTagConstant.SYSTEM_ERROR, invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start, e);
}
} else {
BizApplicationConfig bizApplicationConfig = SpringContextUtil.getBean(BizApplicationConfig.class);
if (!bizApplicationConfig.getDubboLogExclude().contains(invoker.getInterface() + "#" + invocation.getMethodName())) {
//打印日志
log.info("[{}][{}]invoke success:请求={},响应={},耗时={}ms",
invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start);
}
}
return result;
}
}
dubbo客户端统一打印日志类:
@Slf4j
public class ConsumerLogFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long start = System.currentTimeMillis();
Result result = invoker.invoke(invocation);
//请求参数脱敏
List<Object> argList = null;
if (invocation.getArguments() != null) {
argList = Arrays.stream(invocation.getArguments()).map(e -> {
if (e instanceof String) {
return MaskUtil.mask((String) e);
}
return e;
}).collect(Collectors.toList());
}
//返回结果处理
if (result.hasException()) {
log.warn("[{}][{}]invoke fail:请求={},耗时={}ms",
invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), System.currentTimeMillis() - start);
} else {
BizApplicationConfig bizApplicationConfig = SpringContextUtil.getBean(BizApplicationConfig.class);
if (!bizApplicationConfig.getDubboLogExclude().contains(invoker.getInterface() + "#" + invocation.getMethodName())) {
log.info("[{}][{}]invoke success:请求={},响应={},耗时={}ms",
invoker.getInterface(), invocation.getMethodName(),
JSON.toJSONString(argList), JSON.toJSONString(result.getValue()), System.currentTimeMillis() - start);
}
}
return result;
}
}
2.3.断言
常见的断言工具断言和异常是捆绑在一起的(图2.3.1),扩展性差、适用性差
图2.3.1
可以将断言和异常分离开,断言只做判断并对result赋值,抛异常方法判断result,若为false则抛异常,抛什么异常可以自定义(图2.3.2)
图2.3.2
BaseRefer的子类可以自己扩展,抛异常的方法也可以自己扩展,日常开发使用(图2.3.3)
图2.3.3