利用SpringAOP+自定义注解+SpEL实现日志操作记录

背景

在原有项目基础上,增加一个记录用户操作日志的功能。如用户的新增、修改、删除操作,都需要记录到日志中,方便日后分析使用。
要实现这个功能最简单的解决方式就是直接在需要记录日志的地方调用OperateLogService.add()方法,但是这种做法会让代码中嵌入过多与实际业务无关的代码,且各个需要记录日志的地方都需要改动其原有代码,无论是代码的重复利用率还是耦合程度,都不建议如此做。
那么为了优雅的实现日志功能,我们最终选择利用SpringAOP+自定义注解+SpEL表达式来完成。

实现

自定义注解

自定义一个用于标注操作日志的注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 自定义注解-操作日志.
* Created by lll on 17/4/5.
*/
public @interface OperateLog {
/**
* 操作类型
*/
OperateType type();
/**
* 描述信息,支持SpEL表达式
*/
String desc() default "";
}

其中OperateType是个枚举型,用于表示操作类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 日志操作类型
* Created by lll on 17/4/5.
*/
public enum OperateType {
ADD("新增"),
UPDATE("修改"),
DELETE("删除");
private String desc;
OperateType(String desc){
this.desc = desc;
}
public String getDesc(){
return desc;
}
}

在需要记录操作日志的方法上面加上这个注解,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 示例Service
* Created by lll on 17/4/5.
*/
@Service
public class DemoServiceImpl implements DemoService {
@Autowired
private DemoDAO demoDAO;
@Override
@OperateLog(type = OperateType.ADD, desc = "#demo.name") // 这里的desc用的是SpEL表达式
public Long addDemo(Demo demo) {
return demoDAO.insert(demo);
}
}

SpringAOP & SpEL表达式解析

Spring支持在配置文件中配置切面层,也可以自定义标有Aspect注解的类来实现AOP。本例中我们采用后者来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 操作日志AOP切面
* Created by lll on 17/4/5.
*/
@Aspect
@Component
public class OperateLogAspect {
private static final Logger logger = LoggerFactory.getLogger(OperateLogAspect.class);
@Autowired
private OperateLogService operateLogService;
@Pointcut("@annotation(OperateLog)")
public void logAnnotation() {}
@Before("logAnnotation()")
public void before(JoinPoint jp) {
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
String methodName = jp.getSignature().getName();
Object[] arguments = jp.getArgs();
OperateLog logAnnotation = method.getAnnotation(OperateLog.class);
String className = jp.getThis().toString();
OperateType type = logAnnotation.type();
String descSpel = logAnnotation.desc();
Object desc = parseSpel(descSpel, method, arguments); // 解析SpEL表达式
logger.debug("=====================================");
logger.debug("====位于: {}", className);
logger.debug("====调用: {}", methodName);
logger.debug("====type: {}", type);
logger.debug("====desc: {}", desc);
logger.debug("=====================================");
// 添加操作日志记录到数据库中
OperLog operLog = new OperLog();
operLog.setType(type);
operLog.setDesc(desc);
operateLogService.add(operLog);
}
/**
* 解析SpEL表达式
* @param key SpEL表达式
* @param method 反射得到的方法
* @param args 反射得到的方法参数
* @return 解析后SpEL表达式对应的值
*/
private Object parseSpel(String key, Method method, Object[] args){
// 创建解析器
ExpressionParser parser = new SpelExpressionParser();
// 通过Spring的LocalVariableTableParameterNameDiscoverer获取方法参数名列表
LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
// 构造上下文
EvaluationContext context = new StandardEvaluationContext();
if(args.length == parameterNames.length){
for (int i = 0,len = args.length; i < len; i++) {
// 使用setVariable方法来注册自定义变量
context.setVariable(parameterNames[i], args[i]);
}
}
return parser.parseExpression(key).getValue(context);
}
}

说明:

  • 本例以标记了@OperateLog注解的方法为切入点,在方法执行之前,织入增强before方法,将操作记录添加到数据库中。
  • 私有方法parseSpel用于解析SpEL表达式。自定义注解的desc字段支持SpEL表达式,可通过类似前面的DemoService.addDemo(Demo demo)方法注解上的desc传入的#demo.name来获取入参中的值。因此,在解析SpEL表达式之前,需要将请求入参作为自定义变量设置到上下文环境中,SpEL解析器会在解析后从上下文环境中获取对应的值。
  • 获取方法的参数名列表可以通过反射+javassist库的方式获取,不过本例中使用的是Spring提供的LocalVariableTableParameterNameDiscoverer,它是Spring基于ASM框架实现的,相比javassist使用起来会简单许多。

启动Spring对@Aspect注解的支持

最后一步,不要忘记在Spring的配置文件中启动对@Aspect注解的支持

1
2
<!-- 启动对@AspectJ注解的支持 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>

注意:OperateLogAspect是标记了@Component注解的组件,Spring扫包时,需要注意是否在其扫包范围内。

坚持原创技术分享,您的支持将鼓励我继续创作!