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可以一梭子了。??????
原文地址:https://www.cnblogs.com/funmans/p/11019751.html