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 "";
}
枚举类定义:
- 操作来源类型
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;
}
}
- 操作模块
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()
};
}
}
使用测试:
-
日志类中输出一条debug信息
-
controller注解添加
-
打印结果
异步方法调用...日志对象输出 : 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,还烦请指出…