注解+AOP实现日志功能

背景

现有项目有两类日志系统,一个是logback日志,一个是框架层面自己实现的对特定库表数据增删改查操作的日志。但是对于前台的用于生成文档、或是生成库表数据的按钮并没有加以控制,无法得知操作人员进行了哪些操作。项目经理要求加这么个通用模块,也的确很有必要。

思路

  1. 对于这个通用日志模块,首先肯定是要设计一个库表,自行设计即可。用户、模块、方法功能、参数列表、参数值、时间戳等等。
  2. 采用何种方式去实现该功能?系统现有的两个日志框架,对于这个通用日志模块的开发,并无太大用处。需要另辟蹊径。
  3. 实现该功能应该注意哪些?由于是对原有系统功能加操作日志,首先不能影响原有功能的实现,其次不能对原有代码造成过大的侵入性(这也是我拒绝在原有代码中进行将操作日志插入库表的原因),还有不能影响效率等等。
  4. 考虑到这,我的选择是AOP,因为切面的方式可以完美解决上述考虑。
  5. 但是,我们对哪些操作需要进行记录操作日志是有选择的,考虑到项目比较大,总不能指定所有的包吧?所以我选择了使用自定义注解的方式。
  6. 最终,自定义注解+AOP的实现方式被我采纳。

实现

自定义注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.born2do.emsys.annotation;

import java.lang.annotation.*;

/**
* 自定义日志注解
* @author chenhy
* @date 2021/7/1
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAnnotation {

String module(); //模块名

String function(); //功能名

String remark() default ""; //自定义内容
}

切面:

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.born2do.emsys.aop;

import com.born2do.emsys.annotation.LogAnnotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
* @author chenhy
* @date 2021/7/1
*/
@Aspect //表示为切面类
@Component //交由spring去管理
@EnableAspectJAutoProxy(proxyTargetClass = true) //默认为false。true表示使用cglib代理,false表示jdk动态代理
public class LogAnnotationAspect {
//设置切入点(此处为使用LogAnnotation注解的方法)
@Pointcut("@annotation(com.born2do.emsys.annotation.LogAnnotation)")
public void pointcutConfig() {
}

@Before("pointcutConfig()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("前置通知--方法前执行" + joinPoint);
}

@After("pointcutConfig()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("后置通知--方法后执行" + joinPoint);
}

@AfterReturning("pointcutConfig()")
public void doAfterReturning(JoinPoint joinPoint) {
System.out.println("返回通知--调用获得返回值后执行" + joinPoint);
}

@AfterThrowing("pointcutConfig()")
public void doAfterThrowing(JoinPoint joinPoint) {
System.out.println("异常通知--抛出异常后执行" + joinPoint);
}

@Around("pointcutConfig()")
public Object doAround(ProceedingJoinPoint pjp) {
/*result为连接点的放回结果*/
Object result = null;

// 目标类
Class targetClass = pjp.getTarget().getClass();
// 目标类的所有方法
Method[] methods = targetClass.getMethods();
// 切点方法
String methodName = pjp.getSignature().getName();
// 切点方法传入的参数值
Object[] argsValue = pjp.getArgs();

for (Method method : methods) {
//找到切入点对应的方法
if (method.getName().equals(methodName)) {
//拿到方法上的注解对象,获取参数值
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
String module = logAnnotation.module();
String function = logAnnotation.function();
String remark = logAnnotation.remark();

// 获取该切点方法的参数列表
Object[] param = method.getParameters();
}
}

/*前置通知*/
System.out.println("前置通知:目标类名:" + targetClass.getName());

/*执行目标方法*/
try {
result = pjp.proceed();

/*返回通知*/
System.out.println("返回通知:目标方法名" + methodName + ",返回结果为:" + result);
} catch (Throwable e) {
/*异常通知*/
System.out.println("异常通知:目标方法名" + methodName + ",异常为:" + e.getMessage());
}

/*后置通知*/
System.out.println("后置通知:目标方法名" + methodName);

// 基本上所有参数都已经获取,在此处可以进行插入库表等操作

return result;
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.born2do.emsys.controller;

import com.born2do.emsys.annotation.LogAnnotation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* @author chenhy
* @date 2021/6/4
*/
@RestController
public class TestController {

@LogAnnotation(module = "测试模块", function = "用于测试SpringMVC项目框架是否搭建成功")
@RequestMapping("/test")
public String test(@RequestParam("id") int id) {
System.out.println("进入 /test 路径");
return "OK";
}


}

通用日志功能的实现主要依靠于前两个类,当然,在目标方法上添加自定义注解也很重要。

如想运行上述代码,还请自行新建SpringBoot项目,将代码嵌入项目中。

测试结果

浏览器访问 localhost:50000/test?id=1

控制台输出如下日志:

1
2
3
4
5
6
7
前置通知:目标类名:com.born2do.emsys.controller.TestController
前置通知--方法前执行execution(String com.born2do.emsys.controller.TestController.test(int))
进入 /test 路径
返回通知--调用获得返回值后执行execution(String com.born2do.emsys.controller.TestController.test(int))
后置通知--方法后执行execution(String com.born2do.emsys.controller.TestController.test(int))
返回通知:目标方法名test,返回结果为:OK
后置通知:目标方法名test