SpringBoot集成AOP注解形式切面日志

SpringBoot集成AOP注解形式切面日志

前言

​ 最近项目中需要引入自定义日志处理,使用传统的log4j 已不能满足需求…故此,使用了注解形式的日志记录AOP,通过解析SpEL表达式,来获取传入参数,并录入数据库中.

定义注解 :
package com.dabaitu.store.aop.annotation;

import com.dabaitu.store.enums.Module;
import com.dabaitu.store.enums.SourceType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: ZhangLuYang
 * @Date: 2019/12/31 9:35
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OperationLog {

    /**
     * 操作描述
     *
     * @return 操作内容描述
     */
    String expression();

    /**
     * 操作模块
     *
     * @return 模块名称 - 定义枚举类
     */
    Module module(); //枚举类型

    /**
     * 操作
     *
     * @return 操作
     */
    String option();

    /**
     * 操作来源
     *
     * @return 操作来源 - 通过枚举定义,可指定多端
     */
    SourceType sourceType(); //枚举类型

    /**
     * 此值可用于解析SpEl表达式获取方法.
     *
     * @return method返回值
     */
    String value() default "";

}
枚举类定义:
  1. 操作来源类型
package com.dabaitu.store.enums;

/**
 * 操作来源类型
 *
 * @Author: ZhangLuYang
 * @Date: 2019/12/31 9:43
 */
public enum SourceType {

    PCSOURCE("PC"),
    APPSOURCE("APP");

    private final String source;

    SourceType(String source) {
        this.source = source;
    }

    public String getSource() {
        return source;
    }
}
  1. 操作模块
package com.dabaitu.store.enums;

/**
 * 操作模块
 *
 * @Author: ZhangLuYang
 * @Date: 2019/12/31 9:39
 */
public enum Module {

    XXXMODULE("菜单");

    private final String desc;

    Module(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }
}
创建AOP切面:
package com.dabaitu.store.aop;

import com.dabaitu.store.config.handler.OperationLogHandler;
import com.dabaitu.store.entity.dto.UserDTO;
import com.dabaitu.store.service.UserService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Objects;
import java.util.Optional;

/**
 * 创建切面
 *
 * @Author: ZhangLuYang
 * @Date: 2019/12/31 10:03
 */

@Aspect
public class OperationLogAspect {

    private final OperationLogHandler operationLogHandler;
    private final UserService userService;

    @Autowired
    @SuppressWarnings("all")
    public OperationLogAspect(OperationLogHandler operationLogHandler,
                              UserService userService) {
        this.operationLogHandler = operationLogHandler;
        this.userService = userService;
    }

    //匹配OperationLog注解类
    @Around("@annotation(com.dabaitu.store.annotation.OperationLog)")
    public Object addLog(ProceedingJoinPoint point) throws Throwable {
        Object result = point.proceed();
        //如果返回值为null,则返回null
        if (Objects.isNull(result)) {
            return null;
        }
        UserDTO userDTO = Optional.ofNullable(this.userService.getLoginUser()).orElse(new UserDTO());
        String username = userDTO.getUsername();
        Long id = userDTO.getId();
        this.operationLogHandler.analysisSpEl(point, username, result, id);
        return result;
    }
}

处理上下文,解析SpEL表达式:
package com.dabaitu.store.config.handler;


import com.dabaitu.store.annotation.OperationLog;
import com.dabaitu.store.entity.pojo.OperationLogger;
import com.dabaitu.store.entity.vo.ResultVO;
import com.dabaitu.store.service.OperationLogService;
import com.dabaitu.store.utils.AOPUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.util.Assert;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 处理日志上下文,解析SpEl表达式
 *
 * @Author: ZhangLuYang
 * @Date: 2019/12/31 11:17
 */
@Slf4j
public class OperationLogHandler {

    private final ApplicationContext applicationContext;
    private final OperationLogService operationLogService;

    //获取SpEl表达式解析器
    private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
    //获取线程锁工具
    private ReentrantLock reentrantLock = new ReentrantLock();
    //申明当前登陆用户
    private String username;
    //申明操作记录
    private OperationLog operationLog;

    @Autowired
    @SuppressWarnings("all")
    public OperationLogHandler(ApplicationContext applicationContext,
                               OperationLogService operationLogService) {
        this.applicationContext = applicationContext;
        this.operationLogService = operationLogService;
    }

    //异步执行此方法-指定线程池调用线程
    @Async("taskExecutor")
    public void analysisSpEl(ProceedingJoinPoint point, String username, Object result, Long id) {
        try {
            //获取锁
            reentrantLock.lock();
            this.username = username;
            //判断result 是否 为 ResultVO类型
            if (result instanceof ResultVO) {
                result = ((ResultVO) result).getData();
            }
            //创建操作日志实体对象
            OperationLogger operationLogger = new OperationLogger();
            //获取AOP连接点处的签名
            MethodSignature signature = (MethodSignature) point.getSignature();
            //获取请求方法参数
            Object[] args = point.getArgs();
            //获取请求参数名称数组
            String[] parameterNames = signature.getParameterNames();
            //获取当前执行方法
            Method method = signature.getMethod();
            //获取方法上的注解类对象
            this.operationLog = method.getAnnotation(OperationLog.class);
            //获取注解对象内表达式:expression (操作描述)
            String spel = this.operationLog.expression();
            //解析SpEl表达式(操作描述)返回-Expression对象
            Expression expression = spelExpressionParser.parseExpression(spel);
            //获取解析SpEL上下文信息对象
            EvaluationContext evaluationContext = this.initEvaluationContext(args, parameterNames, operationLogger);
            //从上下文中获取(操作)
            String operation = spelExpressionParser.parseExpression(this.operationLog.option()).getValue(evaluationContext, String.class);
            //从上下文中获取(操作内容)
            String operationContent = expression.getValue(evaluationContext, String.class);
            //将解析的值SET进OperationLogger实体对象中
            operationLogger.setModule(this.operationLog.module().getDesc());
            operationLogger.setCreateTime(new Date());
            operationLogger.setOperation(operation);
            operationLogger.setOperationContent(operationContent);
            operationLogger.setUserId(id);
            operationLogger.setUsername(this.username);
            operationLogger.setSourceType(this.operationLog.sourceType().getSource());
            //无法从请求参数中获取到source_id时,从当前方法返回值中通过反射获取。
            if (null == operationLogger.getSourceId()) {
                //result            当前方法返回值
                //operationLogger   操作日志对象
                Object sourceId = FieldUtils.getField(result.getClass(), "id", true).get(result);
                Assert.notNull(sourceId, "OperationLog['source_id'] Could Not Be Null!");
                operationLogger.setSourceId((Long) sourceId);
            }
            operationLogService.insert(operationLogger);
        } catch (Throwable e) {
            log.error("SpEL表达式解析出错了: {} ", e.getMessage());
        } finally {
            //释放锁
            reentrantLock.unlock();
        }
    }

    /**
     * 将请求参数放入标准上下文,并根据请求参数或方法返回值获取source_id
     *
     * @param args            请求参数的值
     * @param parameterNames  请求参数名称
     * @param operationLogger 操作日志对象
     * @return EvaluationContext
     */
    private StandardEvaluationContext initEvaluationContext(
            Object[] args, String[] parameterNames, OperationLogger operationLogger) {
        //创建环境变量对象
        StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext();
        //遍历所有请求参数名称
        for (int i = 0; i < parameterNames.length; i++) {
            //将所有请求参数值 - 请求参数名称 一 一存入上下文中
            String pName = parameterNames[i]; //请求参数名称
            Object pValue = AOPUtils.getTarget(args[i]); //传入请求参数
            //插入到上下文环境中
            standardEvaluationContext.setVariable(pName, pValue);
        }
        //将上下文存入standardEvaluationContext对象中
        standardEvaluationContext.setBeanResolver(new BeanFactoryResolver(applicationContext));
        //将当前用户存入上下文中
        standardEvaluationContext.setVariable("username", username);
        //获取rootSpEL表达式
        String rootSpEL = this.operationLog.value();
        //判断是否value是否为空
        if (StringUtils.isNotBlank(rootSpEL)) {
            //解析SpEL表达式
            Expression rootExpression = this.spelExpressionParser.parseExpression(rootSpEL);
            //通过环境变量计算值并存入环境变量中
            standardEvaluationContext.setRootObject(rootExpression.getValue(standardEvaluationContext));
        }
        //从上下文中解析出id值
        Long id = spelExpressionParser.parseExpression("#id").getValue(standardEvaluationContext, Long.class);
        if (null != id) {
            //插入到上下文环境中
            operationLogger.setSourceId(id);
            return standardEvaluationContext;
        }
        //若还未得到Id则从形参中遍历获取
        Arrays.stream(parameterNames).filter(pN -> {
            Expression expression = spelExpressionParser.parseExpression("#" + pN + "?.id");
            return getId(standardEvaluationContext, expression) != null;
            //若不为null则继续遍历
        }).findFirst().ifPresent(pN -> {
            String sourceId = Objects.requireNonNull(spelExpressionParser.parseExpression("#" + pN + "?.id").getValue(standardEvaluationContext)).toString();
            operationLogger.setSourceId(id);
        });
        return standardEvaluationContext;
    }

    //解析SpEL表达式,若未获取到Id则返回null;
    private Object getId(StandardEvaluationContext standardEvaluationContext, Expression expression) {
        Object o = null;
        try {
            o = expression.getValue(standardEvaluationContext);
        } catch (Throwable throwable) {
            log.error("parse id error:{}", throwable.getMessage());
        }
        return o;
    }

}
获取动态代理Utils类:
package com.dabaitu.store.utils;

import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.support.AopUtils;

import java.lang.reflect.Field;

/**
 * @Author: ZhangLuYang
 * @Date: 2019/12/31 14:56
 */
public class AOPUtils {

    //获取Cglib动态代理目标对象
    private static Object getCglibProxyTargetObject(Object proxy) {
        Field h;
        try {
            h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
            h.setAccessible(true);
            Object dynamicAdvisedInterceptor = h.get(proxy);
            Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
            advised.setAccessible(true);
            return ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
        } catch (Throwable e) {
            return null;
        }
    }

    //获取Jdk动态代理目标对象
    private static Object getJdkDynamicProxyTargetObject(Object proxy) {
        try {
            Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
            h.setAccessible(true);
            AopProxy aopProxy = (AopProxy) h.get(proxy);
            Field advised = aopProxy.getClass().getDeclaredField("advised");
            advised.setAccessible(true);
            return ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
        } catch (Throwable e) {
            return null;
        }
    }

    public static Object getTarget(Object proxy) {
        if (!AopUtils.isAopProxy(proxy)) {
            return proxy;
        }
        if (AopUtils.isJdkDynamicProxy(proxy)) {
            return getJdkDynamicProxyTargetObject(proxy);
        }
        return getCglibProxyTargetObject(proxy);
    }
}
springboot 自动配置的使用:
package com.dabaitu.store.configuration;

import com.dabaitu.store.annotation.NonNullApi;
import com.dabaitu.store.aop.OperationLogAspect;
import com.dabaitu.store.service.impl.OperationServiceImpl;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * 自动配置类: 实现ImportSelector
 * 将全类名注入SpringIOC容器中
 *
 * @Author: ZhangLuYang
 * @Date: 2019/12/31 10:23
 */

@NonNullApi
@Configuration
@Import({OperationLogAutoConfiguration.class})
public class OperationLogAutoConfiguration implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{
                OperationLogHandler.class.getName(),
                OperationLogAspect.class.getName(),
                OperationServiceImpl.class.getName()
        };
    }
}
使用测试:
  1. 日志类中输出一条debug信息在这里插入图片描述

  2. controller注解添加images

  3. 打印结果

异步方法调用...日志对象输出 : OperationLogger(id=null, module=菜单, operation=测试, operationContent=testUser这是一条测试日志, sourceId=10008, sourceType=PC, userId=10000, username=admin, createTime=Tue Dec 31 17:16:00 CST 2019)

函数异步调用成功: 日志对象输出正确.

若存在bug,还烦请指出…

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值