package cn.finopen.boot.autoconfigure.aop;@Configuration
@EnableAspectJAutoProxy
@Order
public class EndpointLogAopConfiguration {/*** 请求方法白名单*/private static final String[] METHOD_WHITE_LIST = {"get", "unreadCount", "find", "findAll"};/*** 防止表单重复提交*/@Aspect@Service@Slf4jpublic static class EndpointLogAspect {@Pointcut("execution(* *.*.boot.autoconfigure..*.endpoint.*.*(..))")public void endpointPointcut() {}@Resourceprivate SaleGroupUserLogApiService logApiService;@Resourceprivate WechatService wechatService;@Resourceprivate StorageDataProperties properties;@Around("endpointPointcut()")public Object doAround(ProceedingJoinPoint pjp) throws Throwable {long start = System.currentTimeMillis();try {Object[] args = pjp.getArgs();Class<?> targetCls = pjp.getTarget().getClass();MethodSignature methodSignature = (MethodSignature) pjp.getSignature();String simpleName = targetCls.getSimpleName();String method = methodSignature.getName();if (method.startsWith("find")) {return proceed(pjp);}for (String i : METHOD_WHITE_LIST) {if (method.equals(i)) {return proceed(pjp);}}String targetObjectMethodName = simpleName + "." + method;String targetMethodParams = Arrays.toString(args);if (targetMethodParams.startsWith("[{") && targetMethodParams.endsWith("}]")) {JSONObject jsonObject;try {jsonObject = JSON.parseObject(targetMethodParams.substring(targetMethodParams.indexOf("{"), targetMethodParams.lastIndexOf("}") + 1));String loginUserId = jsonObject.getString("loginUserId");String loginGroupId = jsonObject.getString("loginGroupId");String loginGroupName = jsonObject.getString("loginGroupName");String requestIp = jsonObject.getString("requestIp");String body = JSONObject.toJSONString(jsonObject, SerializerFeature.WriteNullListAsEmpty);if (body.length() > 1024) {//内容过长截断body = body.substring(0, 1024);}if (loginGroupId != null && loginUserId != null) {logApiService.create(SaleGroupUserLogCreateReq.newBuilder().setLoginGroupId(Long.parseLong(loginGroupId)).setLoginUserId(loginUserId).setLoginGroupName(loginGroupName).setReqUrl(targetObjectMethodName).setReqIp(requestIp).setReqBody(body).build());}} catch (Exception ignore) {return proceed(pjp);}}return proceed(pjp);} catch (Throwable e) {throw print(e, pjp);} finally {long take = System.currentTimeMillis() - start;if (take >= 1000) {if (log.isWarnEnabled()) {log.warn("around endpoint [{}] take {}ms", pjp.getTarget().getClass().getSimpleName(), (System.currentTimeMillis() - start));}} else {if (log.isInfoEnabled()) {log.info("around endpoint [{}] take {}ms", pjp.getTarget().getClass().getSimpleName(), (System.currentTimeMillis() - start));}}}}/*** 为了捕获异步异常** @param pjp* @return* @throws Throwable*/private Object proceed(ProceedingJoinPoint pjp) throws Throwable {String url = properties.getUrl();
// 测试环境屏蔽boolean isTest = url.startsWith("https://test") || url.startsWith("http://test");Object result = pjp.proceed();if (isTest) {return result;}//这里是异步请求,所以直接用try cache 是无效的if (result instanceof Flux) {Flux<?> r = (Flux<?>) (result);return r.onErrorMap(Throwable.class, throwable -> print(throwable, pjp));} else if (result instanceof Mono) {Mono<?> r = (Mono<?>) (result);return r.onErrorMap(Throwable.class, throwable -> print(throwable, pjp));}return result;}private final static String WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=*";/*** 发送异常信息到企业微信** @param e* @param pjp*/private Throwable print(Throwable e, ProceedingJoinPoint pjp) {
// if (e instanceof FibException) {
// return e;
// }ThreadPoolUtils.execute(() -> {Object[] args = pjp.getArgs();Class<?> targetCls = pjp.getTarget().getClass();MethodSignature methodSignature = (MethodSignature) pjp.getSignature();String simpleName = targetCls.getSimpleName();String method = methodSignature.getName();String targetObjectMethodName = simpleName + "." + method;String targetMethodParams = format(Arrays.toString(args));if (targetMethodParams.length() >= 512) {targetMethodParams = targetMethodParams.substring(0, 512);}String errorMessage = getStackTrace(e);String content = "<font color=\"info\">接口地址:</font>" + targetObjectMethodName + "\n<font color=\"info\">请求报文:</font>" + targetMethodParams + "\n<font color=\"warning\">异常消息:</font>" + errorMessage;//markdown内容,最长不超过4096个字节,必须是utf8编码if (content.length() >= 2048) {content = truncateUtf8(content);}WechatWebhookReq webhookReq = WechatWebhookReq.newBuilder().setUrl(WEBHOOK_URL).setMsgType("markdown").setContent(content).build();wechatService.send(webhookReq);});return e;}private String format(String targetMethodParams) {if (targetMethodParams.startsWith("[{") && targetMethodParams.endsWith("}]")) {try {JSONObject jsonObject = JSON.parseObject(targetMethodParams.substring(targetMethodParams.indexOf("{"), targetMethodParams.lastIndexOf("}") + 1));return JSONObject.toJSONString(jsonObject, SerializerFeature.WriteNullListAsEmpty);} catch (Exception ignore) {return targetMethodParams;}} else {return targetMethodParams;}}/**打印完整的日志信息**/String getStackTrace(Throwable e) {StringWriter sw = new StringWriter();PrintWriter pw = new PrintWriter(sw, true);// 打印当前异常的堆栈信息e.printStackTrace(pw);// 遍历并打印所有被抑制的异常(如果有)for (Throwable suppressed : e.getSuppressed()) {suppressed.printStackTrace(pw);}// 递归打印异常的原因链(如果存在)Throwable cause = e.getCause();while (cause != null) {cause.printStackTrace(pw);cause = cause.getCause();}return sw.toString();}/*** 截取UTF-8编码的字符串,确保字节长度不超过指定的最大值。** @param str 待截取的字符串* @return 截取后的字符串*/private final static int MARK_DOWN_MAX = 4096;/*** 简单地根据字节数截断UTF-8编码的字符串,可能会导致多字节字符被截断。** @param str 待截取的字符串* @return 截取后的字符串*/public static String truncateUtf8(String str) {byte[] bytes = str.getBytes(StandardCharsets.UTF_8);if (bytes.length <= MARK_DOWN_MAX) {return str;}// 直接截取到指定字节数对应的字符位置return new String(bytes, 0, MARK_DOWN_MAX, StandardCharsets.UTF_8);}}}