背景

在现代应用系统中,事件审计是一个至关重要的功能。通过记录用户的操作行为,我们可以追踪问题、分析用户行为,甚至在出现安全问题时提供关键证据。由于目前没有较好的事件审计框架,笔者决定实现一套可扩展的事件审计组件,要求对业务低侵入性,可以轻松获取前后变更的内容。

目标

提供自定义注解给业务侧,实现开箱即用的事件审计存储功能。

实现

审计

我们定义了 @EventAuditor 注解,关键字段包括 operator(操作人)、content(操作内容模板)、bizScenario(事件发生的场景)等。

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
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface EventAuditor {

/**
* 操作对象,支持 SpEL 表达式
*
* @return 操作对象
*/
String operator() default Strings.EMPTY;

/**
* 操作角色,支持 SpEL 表达式
* <br/> 用于区分运营人员和用户
*
* @return 操作角色
*/
String role() default Strings.EMPTY;

/**
* 业务场景,支持 SpEL 表达式
* <br/> 推荐风格:产品线+用例+场景
*
* @return 业务场景
*/
String bizScenario() default Strings.EMPTY;

/**
* 记录内容
*
* @return 记录内容,支持 SpEL 表达式
*/
String content() default Strings.EMPTY;

/**
* 额外信息
* <br/> 防止记录的内容过长无法展示,额外序列化保存
*
* @return 额外信息,支持 SpEL 表达式
*/
String extra() default Strings.EMPTY;

/**
* 触发条件
*
* @return 触发条件,为空表示默认触发
*/
String condition() default Strings.EMPTY;

/**
* 调用方法之前解析 SpEL 参数
* <br/>
*
* @return 是否提前解析
*/
boolean evalBeforeInvoke() default true;

/**
* 是否记录返回值
* <br/> 防止记录查询类返回的 List 大对象导致 OOM
*
* @return 是否记录
*/
boolean recordReturnValue() default false;
}

通过 Spring 的 MethodInterceptor,我们实现了 EventAuditorInterceptor 来捕获 @EventAuditor 注解,并在方法调用前后进行审计记录。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
@RequiredArgsConstructor
@Slf4j
public class EventAuditorInterceptor implements MethodInterceptor {

private static final String RETURN = "_return";

private static final String ERROR_MSG = "_errorMsg";

private final EventAuditorConfig eventAuditorConfig;

private AsyncTaskExecutor asyncTaskExecutor;

/**
* 方法调用拦截处理
*
* @param invocation 方法调用元信息
* @return 返回值
* @throws Throwable 异常
*/
@Override
public Object invoke(@NotNull MethodInvocation invocation) throws Throwable {
if (AopUtils.isAopProxy(invocation.getThis())) {
return invocation.proceed();
}

LocalDateTime now = LocalDateTime.now();
Method method = invocation.getMethod();
EventAuditor[] eventAuditors;
try {
eventAuditors = method.getAnnotationsByType(EventAuditor.class);
} catch (Throwable throwable) {
// 如果解析异常,直接执行返回
return invocation.proceed();
}

List<AuditingEvent> events = Lists.newArrayList();
events.addAll(parseList(invocation, eventAuditors, true));

Object result = null;
boolean success = true;
long executionCost = 0L;
String errorMsg = null;
// 执行耗时
String watchId = invocation.getClass() + Strings.DOT + invocation.getMethod().getName();
StopWatch stopWatch = new StopWatch(watchId);
stopWatch.start();
try {
result = invocation.proceed();
} catch (Throwable e) {
if (stopWatch.isRunning()) {
stopWatch.stop();
executionCost = stopWatch.getTotalTimeMillis();
}
success = false;
errorMsg = e.getMessage();
SpelEvaluationContext.setVariable(ERROR_MSG, errorMsg); // 异常信息
throw e;
} finally {
if (stopWatch.isRunning()) {
stopWatch.stop();
executionCost = stopWatch.getTotalTimeMillis();
}
SpelEvaluationContext.setVariable(RETURN, result); // 返回值处理
events.addAll(parseList(invocation, eventAuditors, false));
for (AuditingEvent event : events) {
event.setSuccess(success);
event.setOperateDate(now);
event.setExecutionCost(executionCost);
if (errorMsg != null) {
event.setThrowable(errorMsg);
}
}

send(events);
SpelEvaluationContext.remove(); // 清理当前线程变量
}
return result;
}

/**
* 异步开启后需要设置 AsyncTaskExecutor
*
* @param asyncTaskExecutor 异步任务执行器
*/
public void setAsyncTaskExecutor(AsyncTaskExecutor asyncTaskExecutor) {
this.asyncTaskExecutor = asyncTaskExecutor;
}

/**
* 发送审计事件
*
* @param events 审计事件列表
*/
private void send(List<AuditingEvent> events) {
String senderType = eventAuditorConfig.getSender().getSenderType();
EventSenderBuilder eventSenderBuilder = ExtensionLoader.getExtensionLoader(EventSenderBuilder.class).getExtension(senderType);
eventSenderBuilder.setEventAuditorConfig(eventAuditorConfig);
EventSender eventSender = eventSenderBuilder.build();
if (eventAuditorConfig.getSender().isAsync() && asyncTaskExecutor != null) {
asyncTaskExecutor.execute(() -> eventSender.send(events));
} else {
eventSender.send(events);
}
}

/**
* 解析 {@code AuditingEvent} 列表
*
* @param invocation 方法调用元信息
* @param eventAuditors 审计注解
* @param evalBeforeInvoke 是否在调用方法之前提前解析
* @return {@code AuditingEvent} 列表
*/
private List<AuditingEvent> parseList(@NotNull MethodInvocation invocation, EventAuditor[] eventAuditors,
boolean evalBeforeInvoke) {
List<AuditingEvent> events = Lists.newArrayList();
for (EventAuditor eventAuditor : eventAuditors) {
if (eventAuditor.evalBeforeInvoke() == evalBeforeInvoke) {
AuditingEvent auditingEvent = this.parseModel(eventAuditor, invocation);
if (auditingEvent != null) {
events.add(auditingEvent);
}
}
}
return events;
}

/**
* 解析模型
*
* @param eventAuditor 事件审计注解
* @param invocation 方法调用元信息
* @return 目标模型
*/
private AuditingEvent parseModel(EventAuditor eventAuditor, MethodInvocation invocation) {
EvaluationContext context = SpelEvaluationContext.getContext();
CustomFunctionRegistrar.register((StandardEvaluationContext) context);
Method method = invocation.getMethod();
String[] parameterNames = SpelExpressionEvaluator.getParameterNameDiscoverer().getParameterNames(method);
Object[] arguments = invocation.getArguments();
if (parameterNames != null) {
int len = parameterNames.length;
for (int i = 0; i < len; i++) {
context.setVariable(parameterNames[i], arguments[i]);
}
}

ExpressionParser parser = SpelExpressionEvaluator.getExpressionParser();
if (StringUtils.isNotBlank(eventAuditor.condition())) {
Expression expression = parser.parseExpression(eventAuditor.condition());
if (!Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
return null;
}
}

AuditingEvent auditingEvent = new AuditingEvent();
if (StringUtils.isNotBlank(eventAuditor.operator())) {
Expression expression = parser.parseExpression(eventAuditor.operator());
auditingEvent.setOperator(expression.getValue(context, String.class));
}

if (StringUtils.isNotBlank(eventAuditor.role())) {
Expression expression = parser.parseExpression(eventAuditor.role());
auditingEvent.setRole(expression.getValue(context, String.class));
}

if (StringUtils.isNotBlank(eventAuditor.bizScenario())) {
Expression expression = parser.parseExpression(eventAuditor.bizScenario());
auditingEvent.setBizScenario(expression.getValue(context, String.class));
}

if (StringUtils.isNotBlank(eventAuditor.content())) {
Expression expression = parser.parseExpression(eventAuditor.content());
auditingEvent.setContent(expression.getValue(context, String.class));
}

if (StringUtils.isNotBlank(eventAuditor.extra())) {
Expression expression = parser.parseExpression(eventAuditor.extra());
Object extra = expression.getValue(context, Object.class);
auditingEvent.setExtra(extra instanceof String ? (String) extra : JSONHelper.json().toJSONString(extra));
}
return auditingEvent;
}
}

上述代码中有 3 个关键类,分别是:

  1. SpelExpressionEvaluator :处理 SpEL 表达式。
  2. CustomFunctionRegistrar:存储自定义函数和 Java 方法的关联关系,解析自定义函数时,通过缓存找到对应的 Java 方法反射调用。
  3. EventSenderBuilder :将事件内容往其他渠道发送,例如消息队列、数据库,便于存储。

接下来依次展开说明。

SpEL 表达式解析实现

SpEL 是 Spring 特有的表示解析格式,内部通过 StandardEvaluationContext 实现,我们可以在这个基础上进行封装。

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
public class SpelExpressionEvaluator {

private static final ExpressionParser PARSER = new SpelExpressionParser();

private static final ParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

public static ExpressionParser getExpressionParser() {
return PARSER;
}

public static ParameterNameDiscoverer getParameterNameDiscoverer() {
return DISCOVERER;
}

/**
* 解析 SpEL 表达式
*
* @param expressionString SpEL 表达式
* @param method 方法
* @param arguments 参数
* @return 解析后的内容
*/
public static String parseExpression(String expressionString, Method method, Object[] arguments) {
String[] params = DISCOVERER.getParameterNames(method);
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
SpelEvaluationContext.setVariable(params[i], arguments[i]);
}
}

Expression expression = PARSER.parseExpression(expressionString);
return expression.getValue(SpelEvaluationContext.getContext(), String.class);
}
}

public class SpelEvaluationContext {

private static final TransmittableThreadLocal<EvaluationContext> VARIABLES =
new TransmittableThreadLocal<EvaluationContext>() {

@Override
protected EvaluationContext initialValue() {
return new StandardEvaluationContext();
}
};

public static EvaluationContext getContext() {
return VARIABLES.get();
}

public static Object lookupVariable(String key) {
EvaluationContext context = getContext();
return context.lookupVariable(key);
}

public static void setVariable(String key, Object value) {
EvaluationContext context = getContext();
context.setVariable(key, value);
VARIABLES.set(context);
}

public static void remove() {
VARIABLES.remove();
}
}

自定义函数解决上下文历史

通过 MethodInterceptor 方法拦截只能获取入参和返回值,对于修改内容的场景,没办法获取原始记录。我们引入了 CustomFunctionRegistrar 自定义函数,由用户来决定调用哪个 Java 方法。

在应用启动时,基于 Spring 自动扫描所有标记了 CustomFunctionRegistrar 的代码,通过 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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
@Slf4j
public class CustomFunctionRegistrar implements ApplicationContextAware {

private static final Map<String, Method> FUNCTION_CACHE = new ConcurrentHashMap<>();

/**
* 初始化
*
* @param applicationContext ApplicationContext
* @throws BeansException 初始化Bean异常
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
initialize(applicationContext);
}

/**
* 初始化自定义函数
*
* @param applicationContext ApplicationContext
*/
private void initialize(ApplicationContext applicationContext) {
Map<String, Object> components = applicationContext.getBeansWithAnnotation(Component.class);
components.values().forEach(component -> {
Method[] methods = component.getClass().getMethods();
if (methods.length == 0) {
return;
}

Object targetObject = AopUtils.getDynamicProxyTargetObject(component);
for (Method method : methods) {
CustomFunction annotation = AnnotatedElementUtils.findMergedAnnotation(method, CustomFunction.class);
if (annotation == null) {
continue;
}
if (ReflectionUtils.isStaticMethod(method)) {
cache(annotation, method);
continue;
}
ClassPool pool = ClassPool.getDefault();
Class<?> targetClass = targetObject.getClass();
String staticallyClassName = targetClass.getName() + "_Statically";
Class<?> delegateClass;
CtClass ctClass = pool.getOrNull(staticallyClassName);
try {
if (ctClass == null) {
ctClass = constructCtClass(method, pool, targetClass, staticallyClassName);
delegateClass = ctClass.toClass();
} else {
delegateClass = ctClass.getClass().getClassLoader().loadClass(staticallyClassName);
}
Object proxy = delegateClass.getConstructor(targetClass).newInstance(component);
Method[] proxyMethods = proxy.getClass().getDeclaredMethods();
Arrays.stream(proxyMethods).forEach(proxyMethod -> {
if (Arrays.equals(method.getParameterTypes(), proxyMethod.getParameterTypes()) &&
method.getName().equals(proxyMethod.getName())) {
cache(annotation, proxyMethod);
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}

/**
* 注册自定义函数到 SpEL解析上下文
*
* @param context SpEL解析上下文
*/
public static void register(StandardEvaluationContext context) {
FUNCTION_CACHE.forEach(context::registerFunction);
}

private static void cache(CustomFunction annotation, Method method) {
String registerName = StringUtils.hasText(annotation.value()) ? annotation.value() : method.getName();
FUNCTION_CACHE.put(registerName, method);
log.info("Register custom function '{}' as name '{}'", method, registerName);
}

private CtClass constructCtClass(Method method, ClassPool pool, Class<?> targetClass, String staticallyClassName)
throws NotFoundException, CannotCompileException {
CtClass ctClass = pool.makeClass(staticallyClassName);
ctClass.addInterface(pool.get(Serializable.class.getName()));

CtField field = new CtField(pool.get(targetClass.getName()), "delegating", ctClass);
field.setModifiers(javassist.Modifier.STATIC | javassist.Modifier.PROTECTED);
ctClass.addField(field);

CtConstructor constructor = new CtConstructor(new CtClass[]{pool.get(targetClass.getName())}, ctClass);
constructor.setBody("{delegating = $1;}");
ctClass.addConstructor(constructor);

CtMethod getterMethod = new CtMethod(pool.get(targetClass.getName()), "getDelegating", new CtClass[]{}, ctClass);
getterMethod.setModifiers(javassist.Modifier.PUBLIC);
getterMethod.setBody("{return delegating;}");
ctClass.addMethod(getterMethod);

int modifier = method.getModifiers();
modifier |= javassist.Modifier.STATIC;
String methodName = method.getName();
Class<?> returnType = method.getReturnType();
StringBuilder builder = new StringBuilder();
builder.append(chooseModifier(modifier)).append(" ")
.append(returnType.getName()).append(" ")
.append(methodName).append("(");

Class<?>[] parameterType = method.getParameterTypes();
StringBuilder params = null;
for (int i = 0; i < parameterType.length; i++) {
builder.append(parameterType[i].getName()).append(" ");
builder.append("$_").append(i).append(",");
if (params == null) {
params = new StringBuilder();
}
params.append("$_").append(i).append(",");
}
if (params != null) {
builder.delete(builder.length() - 1, builder.length());
params.delete(params.length() - 1, params.length());
}
builder.append(")");
builder.append("{");
if (!returnType.equals(void.class)) {
builder.append("return").append(" ");
}
builder.append("delegating.").append(methodName).append("(");
if (params != null) {
builder.append(params);
}
builder.append(")").append(";");
builder.append("}");

CtMethod ctMethod = CtMethod.make(builder.toString(), ctClass);
ctClass.addMethod(ctMethod);
return ctClass;
}

private String chooseModifier(int modifier) {
StringBuilder builder = new StringBuilder();
if ((modifier & javassist.Modifier.PUBLIC) == javassist.Modifier.PUBLIC) {
builder.append("public").append(" ");
}
if ((modifier & javassist.Modifier.PRIVATE) == javassist.Modifier.PRIVATE) {
builder.append("private").append(" ");
}
if ((modifier & javassist.Modifier.PROTECTED) == javassist.Modifier.PROTECTED) {
builder.append("protected").append(" ");
}
if ((modifier & javassist.Modifier.ABSTRACT) == javassist.Modifier.ABSTRACT) {
builder.append("abstract").append(" ");
}
if ((modifier & javassist.Modifier.STATIC) == javassist.Modifier.STATIC) {
builder.append("static").append(" ");
}
if ((modifier & javassist.Modifier.FINAL) == javassist.Modifier.FINAL) {
builder.append("final").append(" ");
}
return builder.toString();
}
}

审计事件内容存储实现

拦截到审计信息后,接下来要做的事情就是存储。我们定义了 EventSender 接口和 AuditingEvent 审计事件模型。

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
public interface EventSender {

/**
* 发送审计事件列表
*
* @param events 审计事件列表
*/
void send(List<AuditingEvent> events);
}

@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
@Data
public class AuditingEvent {

/** 操作对象 */
private String operator;

/** 操作角色 */
private String role;

/** 操作时间 */
private LocalDateTime operateDate;

/** 业务场景 */
private String bizScenario;

/** 记录内容 */
private String content;

/** 额外信息 */
private String extra;

/** 返回值 */
private String returnValue;

/** 执行耗时 */
private Long executionCost;

/** 执行是否成功 */
private Boolean success;

/** 异常信息 */
private String throwable;
}

在前面的 EventAuditorInterceptor#send() 方法,就是调用了这个接口。

关于接口的实现,我们预留了扩展点,可以本地日志打印、可以发送到 MQ 或者数据库,例如,本地日志打印实现代码。

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
@RequiredArgsConstructor
@Slf4j
public class LoggingEventSender implements EventSender {

public static final String AUDITING_EVENT = "Auditing event: {}";

private final Level level;

/**
* 发送审计事件列表
*
* @param events 审计事件列表
*/
@Override
public void send(List<AuditingEvent> events) {
if (CollectionUtils.isEmpty(events)) {
return;
}

List<String> contents = events.stream()
.map(AuditingEvent::getContent).collect(Collectors.toList());
switch (level) {
case DEBUG:
log.debug(AUDITING_EVENT, contents);
break;
case INFO:
log.info(AUDITING_EVENT, contents);
break;
case WARN:
log.warn(AUDITING_EVENT, contents);
break;
}
}
}

为了简化配置,我们基于 Spring Boot 的自动装配机制进一步封装。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@ConditionalOnProperty(
prefix = EventAuditorProperties.PREFIX,
name = Conditions.ENABLED,
havingValue = Conditions.TRUE,
matchIfMissing = true
)
@AutoConfigureAfter(AsyncTaskExecutionAutoConfiguration.class)
@EnableEventAuditor
@EnableConfigurationProperties(EventAuditorProperties.class)
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@Configuration(proxyBeanMethods = false)
public class EventAuditorAutoConfiguration {

}

@Setter
@Getter
@ConfigurationProperties(prefix = EventAuditorProperties.PREFIX)
public class EventAuditorProperties extends EventAuditorConfig {

public static final String PREFIX = "event-auditor";

private boolean enabled;
}

@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@Slf4j
@Configuration(proxyBeanMethods = false)
public class EventAuditorConfiguration implements ImportAware {

private AnnotationAttributes enableEventAuditor;

@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.enableEventAuditor = AnnotationAttributes.fromMap(
importMetadata.getAnnotationAttributes(EnableEventAuditor.class.getName(), false));
if (this.enableEventAuditor == null) {
log.warn("@EnableEventAuditor is not present on importing class");
}
}

@Bean
public EventAuditorInterceptor eventAuditorInterceptor(ObjectProvider<EventAuditorConfig> eventAuditorConfig,
ObjectProvider<AsyncTaskExecutor> asyncTaskExecutor) {
EventAuditorInterceptor interceptor = new EventAuditorInterceptor(eventAuditorConfig.getIfUnique(EventAuditorConfig::new));
asyncTaskExecutor.ifAvailable(interceptor::setAsyncTaskExecutor);
return interceptor;
}

@Bean
public EventAuditorPointcutAdvisor eventAuditorPointcutAdvisor(EventAuditorInterceptor eventAuditorInterceptor) {
EventAuditorPointcutAdvisor pointcutAdvisor = new EventAuditorPointcutAdvisor();
pointcutAdvisor.setAdviceBeanName("eventAuditorPointcutAdvisor");
pointcutAdvisor.setAdvice(eventAuditorInterceptor);
if (enableEventAuditor != null) {
pointcutAdvisor.setOrder(enableEventAuditor.getNumber("order"));
}
return pointcutAdvisor;
}

@Bean
public CustomFunctionRegistrar customFunctionRegistrar() {
return new CustomFunctionRegistrar();
}
}

@EqualsAndHashCode
@ToString
@Setter
@Getter
public class EventAuditorConfig {

private final Sender sender = new Sender();

@EqualsAndHashCode
@ToString
@Setter
@Getter
public static class Sender {

private String senderType = "logging";

private boolean async = true;

private final Logging logging = new Logging();

private final Kafka kafka = new Kafka();

private final RocketMQ rocketMQ = new RocketMQ();

@EqualsAndHashCode
@ToString
@Setter
@Getter
public static class Logging {

private Level level = Level.INFO;
}

@EqualsAndHashCode
@ToString
@Setter
@Getter
public static class Kafka {

private String topic;
}

@EqualsAndHashCode
@ToString
@Setter
@Getter
public static class RocketMQ {

private String topic;

private String namespace;

private String tags;

private String keys;
}
}
}

从配置类可以看到,我们为消息存储提供了 3 个选项: Logging 本地日志、RocketMQKafka

代码使用示例

假设我们在 UserService#modifyUser() 修改用户信息,需要记录修改了哪些内容。

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
@Service("userService")
public class UserServiceImpl implements UserService {

//...


/**
* 修改用户
*
* @param cmd
*/
@EventAuditor(bizScenario = "'demo.users.getUserById'", operator = "#operator",
content = "'用户' + #cmd.login + '修改了邮箱,从' + #queryOldEmail(#cmd.id) + '修改为' + #cmd.email")
@Transactional(rollbackFor = Exception.class)
@Override
public Response modifyUser(UserModifyCmd cmd) {
return userModifyCmdExe.execute(cmd);
}

/**
* 自定义函数
*
* @param id 用户ID
* @return 数据库值
*/
@CustomFunction("queryOldEmail")
public String queryOldEmail(Long id) {
return this.getUserById(UserByIdQry.builder().id(id).build()).getData().getEmail();
}
}

对应的 application.yaml 配置文件设置如下。

1
2
3
4
5
6
7
8
9
10
11
event-auditor:
enabled: true
sender:
type: logging
# type: kafka
# kafka:
# topic: demo
# type: rocketmq
# rocketmq:
# topic: demo
# namespace: test

因为笔者配置了 event-auditor.sender.type=logging,所以,审计内容输出到控制台日志,如下图,当调用用户信息更新接口,在控制台打印了 用户admin修改了邮箱 字眼。

产出

对业务代码的侵入性极小,扩展性很高,可以轻松实现事件审计的记录,并存储到你想要的位置。

本文涉及的代码完全开源,感兴趣的伙伴可以查阅 eden-event-auditoreden-event-auditor-spring-boot-starter