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

AOP、注解实现日志收集相关推荐

  1. 利用spring AOP注解实现日志管理

    最近刚接手一个项目,在项目的开始阶段,我们的架构师分配了个任务给我,让我利用spring的切面技术做一个日志管理的案例.要求很简单,就是需要记录:谁在什么时候对谁做了什么操作,同时在日志的描述中还要有 ...

  2. spring aop实现日志收集

    概述 使用spring aop 来实现日志的统一收集功能 详细 代码下载:http://www.demodashi.com/demo/10185.html 使用spring aop 来实现日志的统一收 ...

  3. springBoot+AOP收集日志信息,自定义接口实现日志收集

    之前做的ELK日志分析,没有收集到日志,都 是一些没有规则的输出,提取数据也相对复杂, 今天 有时间就稍微实现了一下利用SpringBoot Aop的方式打印想要的数据格式, 第一步,自定义接口, E ...

  4. java aop注解日志记录_springMVC自定义注解,用AOP来实现日志记录的方法

    需求背景 最近的一个项目,在项目基本完工的阶段,客户提出要将所有业务操作的日志记录到数据库中,并且要提取一些业务的关键信息(比如交易单号)体现在日志中. 为了保证工期,在查阅了资料以后,决定用AOP+ ...

  5. 【日志记录】基于AOP实现自定义日志注解,并支持动态设置注解内容

    前言 平时在java项目开发过程中,涉及到记录操作日志的场景很多,有时候大家习惯把操作日志的生成代码写到业务代码中,这样造成了日志和业务代码的耦合性比较高.可维护性也不强,易读性更差,更多的时候是使用 ...

  6. 我们已经不用AOP做操作日志了!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 前言 用户在操作我们系统的过程中,针对一些重要的业务数据进行增删改 ...

  7. 实现打印异常日志_老生常谈SpringAop日志收集与处理做的工具包

    场景 : 使用Spring Aop拦截参数日志目前大部分做法都基本上大同小异,不想日后每个项目工程都写一份这样的Aop拦截处理日志的代码,甚至代码侵入. 我想知道一些相对重要的请求方法的请求参数,响应 ...

  8. 我们已经不用AOP做操作日志了! | 原力计划

    来源 | JAVA葵花宝典 责编 | 王晓曼.Carol  头图 | CSDN下载自东方IC 前言 用户在操作我们系统的过程中,针对一些重要的业务数据进行增删改查的时候,我们希望记录一下用户的操作行为 ...

  9. 如何使用SpringBoot AOP 记录操作日志、异常日志?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:咫尺的梦想_w cnblogs.com/wm-dv/ ...

最新文章

  1. 从零入门 Serverless | 教你 7 步快速构建 GitLab 持续集成环境
  2. gulp css 压缩 合并
  3. SpringBoot配置文件-yaml的用法
  4. 双十一,没有买卖就没有伤害!
  5. 构建安全应用程序架构必须考虑的十二问
  6. 【Winhex】狂派入门: Winhex的简单使用教程
  7. NLP-文本挖掘-综述
  8. 2019哪里可以进行高层次人才扶持政策申报?
  9. angular使用jqwidgets注意事项
  10. oracle重做control,Oracle 通过Database Control 向重做日志组中添加成员
  11. 2020 CCPC - 网络选拔赛 签到计划
  12. aoc usb显示器 linux,AOC首款USB连接云显示器
  13. 华为服务器检索信息,裸金属服务器使用标签检索资源
  14. 朴素贝叶斯(Naive Bayes model)
  15. 20201103_如何使用标识符?
  16. python量化 双均线策略(金叉死叉)
  17. 关于微信小程序A 与 微信小程序B 之间的跳转问题
  18. 光纤通信(Optical Fiber Communication)
  19. Discuz仿传奇MA游戏官网社区论坛模板源码
  20. 用ERP系统做数据管理对企业有什么好处?

热门文章

  1. leetcode算法题--最小的k个数
  2. javascript写打地鼠
  3. linux命令查看cpu负载,怎么使用Linux命令查看CPU使用率
  4. cnetos6.2搭建mysql_CentOS 6.2安装配置LAMP服务器(Apache+PHP5+MySQL)
  5. Orecle基本概述(2)
  6. nginx 修改并隐藏版本号
  7. hdu 2842 Chinese Rings 矩阵快速幂
  8. CodeForces 471C MUH and House of Cards
  9. Android setTag()/getTag()
  10. 关于System.Web.Caching的“未将对象引用设置到对象的实例”错误