一、前言
本文介绍一种通过AOP+SPEL来记录操作日志的方法,仅供参考。实现操作日志的方法很多,文章简单记录一种方式,可以直接通过本文实现方法直接对接。
二、前缀知识
1.AOP
AOP(Aspect-Oriented Programming , AOP),面向切面编程。它主要用于将那些与业务逻辑无关的代码(例如日志记录,事务管理,安全等)从业务逻辑中分离出来,以提高代码的模块化程度,降低代码耦合性。否则的话我们经常会在多个地方重复编写一些类似的代码,如果直接编写在业务逻辑中,会导致代码冗余和难以维护。AOP则可以帮助我们将这些重复的部分抽离出来,通过定义切面,在特定的时机(如方法调用前后)执行这些切面的逻辑。
2.SPEL
Spring Expression Language(SpEL)是由 Spring 框架提供的一个强大的表达式语言。它允许你在运行时解析和操作对象、属性、数组、集合和方法调用。SpEL 的设计初衷是通过一种一致且易于使用的方式来配置和操作 Spring 应用程序中的各种组件。
本文之中我们使用的是SPEL在注解中的使用,协助我们获取入参。
使用方法:${#request.propertyName}
三、实现操作日志记录
1.背景
需要对当前模块下的一些操作进行记录,比如商品模块中产品的查看、更新、状态的更新等,我们肯定是不期望每次执行这些操作同时还需要对操作进行记录。所以我们希望可以自动的处理这些东西并且不影响我们的业务主逻辑,不然我们手动记录这些操作记录业务逻辑该有多冗余呀,所以自动实现全局操作日志记录它闪亮登场!
2.实现步骤
1.定义注解@LogInfo
接口中的类为自定义,你需要记录什么就自己定义,我需要操作的实体类modelName,备注remark等,就先进行定义,然后可以在使用注解时候配合SPEL赋值,最终通过解析获取落库。
/**
* 自定义注解-实现操作日志记录
*
* @author ysx
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface LogInfo {
/**
* 操作类
*/
String modelName() default "";
/**
* 备注
*/
String remark() default "";
/**
* 内容
*/
String logContent() default "";
/**
* 操作之前的原数据(SpEL)
*/
String before() default "";
/**
* 操作之后的新数据(SpEL)
*/
String after() default "";
}
2.定义切面
咱们优先取出了切面类,实际上该切面类中包含着其他的自定义类,使用时需要将其也定义出来便不会报错,包含切面信息类,缓存,上下文。
package net.chngc.c2c.item.admin.controller.advice;
import io.terminus.api.request.AbstractRequest;
import io.terminus.common.model.Response;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.chngc.c2c.item.admin.controller.aspectMethodInfo.LogAspectMethodInfo;
import net.chngc.c2c.item.admin.controller.annotation.LogInfo;
import net.chngc.c2c.item.admin.controller.context.LogContext;
import net.chngc.c2c.item.admin.controller.context.LogExpressionEvaluator;
import net.chngc.c2c.item.admin.util.UserInfoUtils;
import net.chngc.c2c.item.api.bean.common.AbstractEnhanceRequest;
import net.chngc.c2c.item.api.bean.request.basic.ItemItemOperateLogCreateRequest;
import net.chngc.c2c.item.api.facade.basic.ItemItemOperateLogFacade;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.spel.support.StandardEvaluationContext;
@Aspect
@Component
@Order(0)
@Slf4j
@RequiredArgsConstructor
public class LogInfoAop extends LogContext {
private final ItemItemOperateLogFacade itemItemOperateLogFacade;
@Pointcut(value = "@annotation(net.chngc.c2c.item.admin.controller.annotation.LogInfo)")//注解所在包
public void pointcut() {}
@Around(value = "pointcut()")
public Object logCreate(ProceedingJoinPoint point) throws Throwable {
if (AopUtils.isAopProxy(point.getTarget())) {
return point.proceed();
}
// 获取目标方法
Method method = getMethodSignature(point).getMethod();
if (method == null) {
return point.proceed();
}
LogInfo annotation = method.getAnnotation(LogInfo.class);
if (annotation == null) {
return point.proceed();
}
// 获取目标真实方法
Object result = point.proceed();
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(point.getTarget());
Object[] args = point.getArgs();
//自定义切面截取方法的信息类
LogAspectMethodInfo logAspectMethodInfo = new LogAspectMethodInfo(targetClass, method, args, getMethodSignature(point).getParameterNames(), annotation);
try {
// 解析信息并记录
recordLog(logAspectMethodInfo);
} catch (Exception e) {
log.error(e.getMessage());
} finally {
clearVariables();
}
if (logAspectMethodInfo.isErrored()) {
log.error(logAspectMethodInfo.getThrown().getMessage());
}
return result;
}
private void recordLog(LogAspectMethodInfo logAspectMethodInfo) {
StandardEvaluationContext standardEvaluationContext = initContext(logAspectMethodInfo);
LogExpressionEvaluator expressionEvaluator = getExpressionEvaluator();
AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(logAspectMethodInfo.getMethod(), logAspectMethodInfo.getTargetClass());
// 拿到注解元信息
LogInfo annotation = logAspectMethodInfo.getAnnotation();
Object logContent = expressionEvaluator.parseExpression(annotation.logContent(), annotatedElementKey, standardEvaluationContext);
Object modelName = expressionEvaluator.parseExpression(annotation.modelName(), annotatedElementKey, standardEvaluationContext);
Object remark = expressionEvaluator.parseExpression(annotation.remark(), annotatedElementKey, standardEvaluationContext);
// TODO 入库/ES
System.out.println("logContent:" + annotation.logContent());
System.out.println("modelName:" + annotation.modelName());
System.out.println("remark:" + remark);
}
/**
* 获取目标方法
*/
private MethodSignature getMethodSignature(ProceedingJoinPoint point) {
Signature signature = point.getSignature();
if (signature instanceof MethodSignature) {
return ((MethodSignature) signature);
}
return null;
}
}
3.截取的切面信息类
该类字段可以自定义使用,我定义的为一些基本的信息,不使用也可以删去
package net.chngc.c2c.item.admin.controller.aspectMethodInfo;
import net.chngc.c2c.item.admin.controller.annotation.LogInfo;
import java.lang.reflect.Method;
/**
* 切面方法基本信息
*/
public class LogAspectMethodInfo {
// 目标类
private final Class<?> targetClass;
// 方法
private final Method method;
// 参数
private final Object[] args;
// 参数名
private final String[] paramNames;
// 返回值
private Object returned;
// 异常信息
private Throwable thrown;
// 方法执行时间
private Long execTimestamp;
// 注解信息
private LogInfo annotation;
public LogAspectMethodInfo(Class<?> targetClass, Method method, Object[] args, String[] paramNames, LogInfo annotation) {
this.targetClass = targetClass;
this.method = method;
this.args = args;
this.paramNames = paramNames;
this.annotation = annotation;
}
public boolean isErrored() {
return this.thrown != null;
}
public Class<?> getTargetClass() {
return targetClass;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
public Object getReturned() {
return returned;
}
public void setReturned(Object returned) {
this.returned = returned;
}
public Throwable getThrown() {
return thrown;
}
public void setThrown(Throwable thrown) {
this.thrown = thrown;
}
public Long getExecTimestamp() {
return execTimestamp;
}
public void setExecTimestamp(Long execTimestamp) {
this.execTimestamp = execTimestamp;
}
public LogInfo getAnnotation() {
return annotation;
}
public void setAnnotation(LogInfo annotation) {
this.annotation = annotation;
}
public String[] getParamNames() {
return paramNames;
}
}
4.定义缓存器
为了使该实现拥有更好的性能,所以我们使用缓存 ,将SPEL表达式进行缓存,增强效率
package net.chngc.c2c.item.admin.controller.context;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class LogExpressionEvaluator extends CachedExpressionEvaluator {
/**
* 表达式模板
*/
static class TemplateParserContext implements ParserContext {
public static final String beginStr = "${";
public static final String endStr = "}";
public String getExpressionPrefix() {
return beginStr;
}
public String getExpressionSuffix() {
return endStr;
}
public boolean isTemplate() {
return true;
}
}
private final Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);
private final Map<AnnotatedElementKey, Method> methodCache = new ConcurrentHashMap<>(64);
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public Object parseExpression(String conditionExpr, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
return getExpression(this.expressionCache, methodKey, conditionExpr).getValue(evalContext, Object.class);
}
@Override
protected Expression parseExpression(String expression) {
return getParser().parseExpression(expression, new TemplateParserContext());
}
public StandardEvaluationContext createEvaluationContext(Class<?> targetClass, Method method, Object[] args, BeanFactory beanFactory) {
Method targetMethod = getTargetMethod(targetClass, method);
StandardEvaluationContext context = new MethodBasedEvaluationContext(null, targetMethod, args, parameterNameDiscoverer);
if (beanFactory != null) {
// 要访问工厂bean本身,应该在bean名称前加上&符号
// parser.parseExpression("&foo").getValue(context);
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
return context;
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
return methodCache.computeIfAbsent(methodKey, k -> AopUtils.getMostSpecificMethod(method, targetClass));
}
}
5.定义上下文
package net.chngc.c2c.item.admin.controller.context;
import net.chngc.c2c.item.admin.controller.aspectMethodInfo.LogAspectMethodInfo;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.HashMap;
import java.util.Map;
public class LogContext implements BeanFactoryAware {
// 定义Evaluator 内含Evaluator
private final static LogExpressionEvaluator expressionEvaluator = new LogExpressionEvaluator();
protected BeanFactory beanFactory;
// 用户定义的变量
private static final ThreadLocal<Map<String, Object>> VARIABLES = new ThreadLocal<Map<String, Object>>() {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
public StandardEvaluationContext initContext(LogAspectMethodInfo logAspectMethodInfo) {
StandardEvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(logAspectMethodInfo.getTargetClass(),
logAspectMethodInfo.getMethod(),
logAspectMethodInfo.getArgs(),
beanFactory
);
// 可以把返回值设置进spel变量中,或者自定义全局的变量
if (logAspectMethodInfo.getReturned() != null) {
evaluationContext.setVariable("_returned", logAspectMethodInfo.getReturned());
}
// 把所有变量放入上下文
evaluationContext.setVariables(getVariables());
return evaluationContext;
}
public LogExpressionEvaluator getExpressionEvaluator() {
return expressionEvaluator;
}
public static Map<String, Object> getVariables() {
return VARIABLES.get();
}
public static void clearVariables() {
VARIABLES.remove();
}
public static void setVariable(String key, String variable) {
getVariables().put(key, variable);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
6.示例
至此,我们的配置已经完成了,我们进行一下测试。
对于属性的更新我们记录一下该操作,将主要信息放在remark中。
@ApiOperation("更新商品属性")
@PostMapping(value = "/update")
@LogInfo(remark = "更新属性 :更新为${#request.name}",modelName = "ItemAttributePo")
public Response<Boolean> update(@RequestBody ItemAttributeUpdateRequest request) {
Response<Boolean> response = itemAttributeFacade.update(request);
if (!response.isSuccess()) {
throw new JsonResponseException(response.getError());
}
return response;
}
成功!!