AOP、注解实现日志收集

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

时间: 2024-10-31 02:31:13

AOP、注解实现日志收集的相关文章

javaWEB SSM AOP+注解保存操作日志

本篇文章的诞生离不开这篇文章的作者:http://blog.csdn.net/czmchen/article/details/42392985. 前言 操作日志在javaWeb的业务系统中是在是太常见的功能了,主要记录用户再什么时间,什么位置进行了什么操作.如果每新增一个功能都要写一个插入代码的话,是非常不容易维护的.加一个字段就要在每个插入语句上加入这个字段.所以AOP+注解的优势就显现了出来,不仅如此,当我们有了这套代码以后,可以通用在该系统的wap端或者其他的系统中,不必修改太多的代码.针

Spring aop 记录操作日志 Aspect

前几天做系统日志记录的功能,一个操作调一次记录方法,每次还得去收集参数等等,太尼玛烦了.在程序员的世界里,当你的一个功能重复出现多次,就应该想想肯定有更简单的实现方法.于是果断搜索各种资料,终于搞定了,现在上代码 环境: SpringMvc + myBatis jar包 :      (aspect.jar也行,我原来项目中有,便没有替换了) 1.自定义注解类   ArchivesLog.java(获取Controller描述用的) package com.noahwm.uomp.archive

Spring入门(三)— AOP注解、jdbc模板、事务

一.AOP注解开发 导入jar包 aop联盟包. aspectJ实现包 . spring-aop-xxx.jar . spring-aspect-xxx.jar 导入约束 aop约束 托管扩展类和被扩展类 <!-- 要做AOP, 一定要托管扩展类和被扩展类 --> <bean id="us" class="com.pri.service.impl.UserServiceImpl"></bean> <bean id="

SpringBoot —— AOP注解式拦截与方法规则拦截

AspectJ是一个面向切面的框架,它扩展了Java语言.AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件. SpringBoot中AOP的使用方式主要有两种:注解式拦截与方法规则拦截,具体使用如下文所示. 一.创建一个简单springboot 2.03项目,添加aop依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId

Spring MVC 中使用AOP 进行统一日志管理--XML配置实现

1.介绍 上一篇博客写了使用AOP进行统一日志管理的注解版实现,今天写一下使用XML配置实现版本,与上篇不同的是上次我们记录的Controller层日志,这次我们记录的是Service层的日志.使用的工程还是原来的那个,具体的Spring mvc 工程搭建暂不介绍.上篇记录controller层日志的时候是将切面类组件叫给spring MVC 进行管理,因为 controller 也是交给spring MVC进行管理的,但是记录service 层日志的时候应该就应该再spring 容器中进行了,

Spring boot学习(六)Spring boot实现AOP记录操作日志

前言 在实际的项目中,特别是管理系统中,对于那些重要的操作我们通常都会记录操作日志.比如对数据库的CRUD操作,我们都会对每一次重要的操作进行记录,通常的做法是向数据库指定的日志表中插入一条记录.这里就产生了一个问题,难道要我们每次在 CRUD的时候都手动的插入日志记录吗?这肯定是不合适的,这样的操作无疑是加大了开发量,而且不易维护,所以实际项目中总是利用AOP(Aspect Oriented Programming)即面向切面编程这一技术来记录系统中的操作日志. 日志分类 这里我把日志按照面向

传统ELK分布式日志收集的缺点?

传统ELK图示: 单纯使用ElK实现分布式日志收集缺点? 1.logstash太多了,扩展不好. 如上图这种形式就是一个 tomcat 对应一个 logstash,新增一个节点就得同样的拥有 logstash,可以说很浪费了. 2.读取IO文件,可能会产生日志丢失. 3.不是实时性 比如logstash,底层通过定时器发现数据库发生变化后才去同步,由于是定时则必然出现延迟. 那么既然出现了这些问题,有什么解决方案呢? 安装kafka kafka是基于发布订阅模式的,类似于生产者与消费者. 一张图

SpringBoot使用AOP记录请求日志和异常日志

本文基础WEB环境使用SpringBoot及Spring-Data-Jpa构建 关于SpringBoot及JPA用法本文不再过多讨论,下面是引入的jar: 起步依赖 web及aop依赖 jpa依赖 mysql驱动 本文着重AOP的使用,你也可以使用自己构建的环境 由于本文中使用的JPA,因此首先创建保存日志及保存异常的实体类,如果你使用的是Mybatis,则需要首先创建表然后根据表来逆向生成实体类 保存日志的实体类: 日志实体 保存异常的实体类: 异常实体 接下来我们定义一个Operation注

spring中的aop注解(整合junit测试)

使用spring中的aop前先来了解一下spring中aop中的一些名词 Joimpoint(连接点):目标对象中,所有可能增强的方法 PointCut(切入点):目标对象,已经增强的方法 Advice(通知/增强):增强的代码 Target(目标对象):被代理对象 Weaving(织入):将通知应用到切入点的过程 Proxy(代理):将通知织入到目标对象之后,形成代理对象 aspect(切面):切入点+通知 一:不使用spring的aop注解 以javaEE中的service层为例 UserS