博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
AOP、注解实现日志收集
阅读量:6069 次
发布时间:2019-06-20

本文共 12059 字,大约阅读时间需要 40 分钟。

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 objectList = Arrays.asList(context.getJoinPoint().getArgs());            List 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
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
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补充:

/** * 日志处理上下文 */@Datapublic 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 
* @return T */ public static
T parseExpression(String expression, Method method, Object[] args, Class
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
* @return T */ public static
T parseExpression(String expression,Object targetObj,Class
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
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

你可能感兴趣的文章
Sql与C#中日期格式转换总结
查看>>
iOS开发流程总结
查看>>
hadoop datanode 启动出错
查看>>
js颜色拾取器
查看>>
IDEA使用(1)intellIJ idea 配置 svn
查看>>
Thread Safety in Java(java中的线程安全)
查看>>
WPF 降低.net framework到4.0
查看>>
数据管理DMS 全量SQL诊断:你的SQL是健康的蓝色,还是危险的红色?
查看>>
搭建一个通用的脚手架
查看>>
开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
查看>>
开源磁盘加密软件VeraCrypt教程
查看>>
本地vs云:大数据厮杀的最终幸存者会是谁?
查看>>
阿里云公共镜像、自定义镜像、共享镜像和镜像市场的区别 ...
查看>>
shadowtunnel v1.7 发布:新增上级负载均衡支持独立密码
查看>>
IdleHandler,页面启动优化神器
查看>>
Java线程:什么是线程
查看>>
mysql5.7 创建一个超级管理员
查看>>
【框架整合】Maven-SpringMVC3.X+Spring3.X+MyBatis3-日志、JSON解析、表关联查询等均已配置好...
查看>>
要想成为高级Java程序员需要具备哪些知识呢?
查看>>
带着问题去学习--Nginx配置解析(一)
查看>>