1.問題描述
需要對日常使用對接口進行出入參數、請求結果、請求耗時、請求關鍵信息等的記錄
2.解決方案
利用注解標示出接口中的關鍵信息。利用AOP進行方法前后的攔截記錄請求入參以及處理結果。利用SPEL解析參數中的關鍵信息
考慮點:1.各個接口的參數都不一致。自己想要的關鍵信息可能包含在入參中,也可能不包含在入參中。參數中關鍵信息的解析
如:void test(String userId):userId就是需要的關鍵信息
void test(String userName,String userId):userId為關鍵信息
void test(User user):這里的關鍵信息包含在user對象中
void test():這里方法沒有任何參數,但是可能自己需要的關鍵信息userId可能存在於Session或者其他地方。
2.當關鍵信息不再參數中的時候,如何解析:(本文方案是提供Handler接口來輔助進行關鍵信息的獲取)
3.對於敏感信息是否有必要入庫保存,一般來說敏感信息是不允許入庫的,這個時候如何使得入參中的敏感信息不被保存
3.實現
使用實例:
實例1:@Logger(flag = "WX",des = "wxcs",ignore = {"#param.mobile"},value = @Filed(name = "openId",handleClass = WXBaseController.class,method = "getHeader"))
實例2: @Logger(flag = "WX",des = "實例描述",ignore = {"#param.mobile"},
value = {@Filed(name = "openId",handleClass = WXBaseController.class,method = "getHeader"),
@Filed(name = "userId", handleClass = WXBaseController.class,method = "getHeader")})
代碼結構:

3.1 注解
public @interface Logger { Filed[] value() default {}; /** * 日志標示 * @return */ String flag(); /** * 日志描述 * @return */ String des(); /** * 忽略i 字段 * @return */ String[] ignore() default {}; /** * 結果處理類 * @return */ Class<?> resultClass() default ResultHandler.class; /** * 結果處理方法 * @return */ String resultMethod() default "getResponseResult"; /** * 結果處理參數 * @return */ Class[] resultType() default Object.class; }
|
屬性名
|
是否必填
|
描述
|
備注
|
|---|---|---|---|
| value | 是 | 用於描述需要記錄日志的關鍵字段 | |
| returnType | 否 | 結果處理方法參數類型 | |
resultMethod |
否 | 對於接口調用結果進行成功與不成功的處理方法,默認提供 | 默認支持:支持返回狀態以status、code來標示請求結果的 注:如返回格式不是這種類型需要主動實現接口處理類 |
resultClass |
否 | 對於接口調用結果進行成功與不成功的處理類,默認提供 | 默認支持 |
| ignore | 否 | 對於接口參數中的一些機密信息或者不必要信息進行過濾不記錄日志 | 注意:值為EL表達式 如:#user.name、#list[0]等 |
| flag | 是 | 用於給日志一個標示,建議使用英文,以便於日志分析統計 | |
| des | 是 | 對於被收集接口的日志內容描述 |
public @interface Filed { /** * 名稱 * @return */ String name(); /** * 參數字段表達式 * @return */ String value() default "#openId"; /** * 特殊處理類 * @return */ Class<?> handleClass() default Class.class; /** * 特殊處理的函數名,默認不處理 * @return */ String method() default ""; /** * 特殊處理方法參數類型 * @return */ Class<?>[] methodParamType() default {}; }
|
屬性名
|
是否必填
|
描述
|
備注
|
|---|---|---|---|
| name | 是 | 關鍵字段名稱,對應於日志實體中相應字段 | 如:openId 對應 實體中 openId,解析后會將解析結果直接賦值給實體對應屬性 |
| value | 否 | 用於標示想要獲取的關鍵字段值在實體中的位置,EL | 如:#user.id、${user.id}、#list[0]等 |
handleClass |
否 | 對於復雜對象的參數處理類 | 如:需要的關鍵信息在JSON字符串中。不能夠直接拿到。此時需要實現handler來獲取輔助獲取 |
method |
否 | 對於復雜對象的參數處理方法 | handler具體方法的返回值類型可以隨意。但,同樣的需要跟value值配合使用 |
methodParamType |
否 | 對於復雜對象的參數處理方法參數類型 | 參數目的只為在一個類具有多個相同名稱方法時能夠找到正確處理方法,默認無參 |
3.2 具體實現
3.2.1 首先是整個業務執行邏輯,注:正常的業務邏輯異常還是需要給拋出的;日志收集不能夠影響正常邏輯的運行;日志保存須得做成異步的(原因我想都明白)
@Pointcut("@annotation(logger)")
public void pointCut(OpLogger logger) {
}
@Around("pointCut(logger)")
public Object around(ProceedingJoinPoint joinPoint, OpLogger logger) throws Throwable {
HandlerContext context = new HandlerContext(joinPoint, logger, new ActiveLog());
prepare(context);
for (Filed filed : logger.value()) {
filedMapper(context, filed);
}
try {
execute(context);
return context.getResult();
} catch (Throwable e) {
log.error("業務執行異常:", e);
context.setResult(e);
context.setExeError(true);
throw e;
} finally {
parseResult(context);
saveLog(context);
}
}
3.2.2 prepare前處理:注:敏感信息的忽略要注意不可以直接操作入參,需要clone入參的副本,且必須是深復制;否則操作的直接是入參,會導致接口實際入參改變。影響到了正常邏輯,這是我們最不希望看到的。
/** * 前置處理 * @param context */ private void prepare(HandlerContext context) { HttpServletRequest request; try { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); } catch (Exception e) { context.setNext(false); return; } String requestURI = request.getRequestURI(); String ip = IPUtils.getRealIP(request); String userAgent = request.getHeader("user-agent"); context.getLog().setReqUrl(requestURI); context.getLog().setIpAddress(ip); context.getLog().setUserAgent(userAgent); context.getLog().setEventFlag(context.getLogger().flag()); context.getLog().setEventDesc(context.getLogger().des()); context.getLog().setConsumeTime(System.currentTimeMillis()); //處理忽略字段 ignoreParam(context); } private void ignoreParam(HandlerContext context){ try{ List<Object> objectList = Arrays.asList(context.getJoinPoint().getArgs()); List<Object> args = new ArrayList<>(); (new DozerBeanMapper()).map(objectList,args); Method method = ((MethodSignature) context.getJoinPoint().getSignature()).getMethod(); LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); String[] paramNames = discoverer.getParameterNames(method); Map<String,Object> params = new HashMap<>(); if(paramNames != null && paramNames.length>0){ for (int i = 0; i < paramNames.length; i++) { params.put(paramNames[i],args.get(i)); } } for (String filed : context.getLogger().ignore()) { SpelUtils.clearValue(params,filed,null); } context.getLog().setReqParams(JSON.toJSONString(params)); }catch (Exception e){ context.setNext(false); } }
3.2.3 關鍵信息抓取 注:此處跟忽略字段都用到了SPEL表達式。不懂的同學--戳我
/** * 字段映射 * @param context * @param filed */ private void filedMapper(HandlerContext context, Filed filed) { if (!context.isNext()) { return; } ProceedingJoinPoint joinPoint = context.getJoinPoint(); Object[] args = joinPoint.getArgs(); //只處理條件完整的 String param = null; if (StringUtils.isNotBlank(filed.value()) && filed.handleClass() != Class.class && StringUtils.isNotBlank(filed.method())) { try { Method declaredMethod = filed.handleClass().getDeclaredMethod(filed.method(), filed.methodParamType()); declaredMethod.setAccessible(true); param = SpelUtils.parseExpression(filed.value(), declaredMethod.invoke(filed.handleClass().newInstance(), filed.methodParamType().length > 0 ? args : null), String.class); } catch (Exception e) { context.setNext(false); } } else if (StringUtils.isNotBlank(filed.value())) { try { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); param = SpelUtils.parseExpression(filed.value(), method, args, String.class); } catch (Exception e) { context.setNext(false); } } Class<? extends ActiveLog> log = context.getLog().getClass(); Field logField; try { logField = log.getDeclaredField(filed.name()); logField.setAccessible(true); logField.set(context.getLog(), param); } catch (Exception e) { context.setNext(false); } }
3.2.4 其他邏輯 注:前文提到有的關鍵信息不再接口參數中,整個時候需要Handler來處理,但是Handler處理的結果可能還是一個對象,關鍵信息還在這個對象中間。這個時候同樣需要注解中配置的el表達式來從Handler返回結果中來解析關鍵信息。
/** * 執行正常邏輯 */ private void execute(HandlerContext context) throws Throwable { ProceedingJoinPoint joinPoint = context.getJoinPoint(); Object result = joinPoint.proceed(joinPoint.getArgs()); context.setResult(result); } /** * 結果處理 * @param context */ private void parseResult(HandlerContext context) { if (!context.isNext()) { return; } if (context.isExeError()) { context.getLog().setRespResult(((Exception) context.getResult()).getMessage()); context.getLog().setRespFlag(RespResult.EXCEPTION.name()); context.getLog().setConsumeTime(null); return; } Class<?> resultClass = context.getLogger().resultClass(); String resultMethod = context.getLogger().resultMethod(); if (resultClass != Class.class && StringUtils.isNotBlank(resultMethod)) { try { Method resultClassDeclaredMethod = resultClass.getDeclaredMethod(resultMethod, context.getLogger().resultType()); Object stringResult = resultClassDeclaredMethod.invoke(resultClass.newInstance(), context.getResult()); context.getLog().setRespResult(JSON.toJSONString(context.getResult())); context.getLog().setRespFlag(stringResult.toString()); context.getLog().setConsumeTime(System.currentTimeMillis() - context.getLog().getConsumeTime()); } catch (Exception e) { context.setNext(false); } } } /** * 保存日志 * @param context */ private void saveLog(HandlerContext context) { if (!context.isNext()) { return; } saveHandler.saveLog(context.getLog()); }
4補充:
/** * 日志處理上下文 */ @Data public class HandlerContext { /** * 是否可以繼續構建日志 */ private boolean next = true; private ProceedingJoinPoint joinPoint; private OpLogger logger; private ActiveLog log; private Object result; private boolean exeError; public HandlerContext(ProceedingJoinPoint joinPoint, OpLogger logger, ActiveLog log) { this.joinPoint = joinPoint; this.logger = logger; this.log = log; } }
工具類:
/** * 解析方法參數 * @param expression * @param method * @param args * @param classType * @param <T> * @return T */ public static <T> T parseExpression(String expression, Method method, Object[] args, Class<T> classType) { if (StringUtils.isBlank(expression)) { return null; } else if (!expression.trim().startsWith("#") && !expression.trim().startsWith("$")) { return null; } else { LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); String[] paramNames = discoverer.getParameterNames(method); if (ArrayUtils.isEmpty(paramNames)) { return null; } else { StandardEvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < paramNames.length; ++i) { context.setVariable(paramNames[i], args[i]); } return (new SpelExpressionParser()).parseExpression(expression).getValue(context, classType); } } } /** * 解析指定對象參數 * @param expression * @param targetObj * @param classType * @param <T> * @return T */ public static <T> T parseExpression(String expression,Object targetObj,Class<T> classType){ if(targetObj != null){ StandardEvaluationContext context = new StandardEvaluationContext(); String prefix = "target"; context.setVariable(prefix,targetObj); if(StringUtils.isBlank(expression)){ expression = "#" + prefix; }else{ expression = "#" + prefix +"." + expression.substring(expression.indexOf("#")+1); } return (new SpelExpressionParser()).parseExpression(expression).getValue(context, classType); }else{ return null; } } /** * 根據表達式指定字段值 */ public static void clearValue(Map<String,Object> params,String expression,Object value){ if(StringUtils.isNotBlank(expression) && params != null && !params.isEmpty()){ StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariables(params); (new SpelExpressionParser()).parseExpression(expression).setValue(context, value); } }
以上就是整個日志收集的大概過程以及大致代碼。實際上利用 注解 以及 AOP 還有很多事情是可以做的,比如簡化Kafka的操作、簡化分布式鎖的開發成本等等。
在SpringBoot如此流行的今天。想想這些繁瑣的事情都能夠也將變成各種 Starter 了。真好。。。又TM可以一梭子了。😂😂😂
