首先明确需要明确一下业务场景,对于每个接口的请求日志,我们业务是统一在网关层进行记录处理,本文描述的是指业务在执行过程中,产生的业务操作记录,一般是指事务型操作的执行,往往伴随着业务状态的转变,主要用于判断
当前业务的进行轨迹及相应的操作信息
。
对于我们的业务系统来说,业务是一个众包接单平台,其中各类业务状态的的转换很多,很多业务需求需要记录业务状态的转换,如果每种业务转换都单独写操作记录的拼装存储逻辑,缺点会很明显:
1.核心逻辑和记录操作逻辑耦合,不符合单一职责
2.开发代码繁琐,每次记录都需要把拼装存储逻辑写一遍,无法逻辑复用
3.十分不优雅
基于这些情况,很容易就可以想到通过切面编程也就是AOP对核心代码及非关注点的代码进行分离解耦。
那么对于一个SpringBoot项目,一般编写AOP逻辑步骤如下
1. 定义一个注解
2. 编写切面逻辑,切点、横切逻辑、织入
3. 使用注解
定义一个注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OpLogRecord {
/**
* 业务Id
* @return 业务Id
*/
String bizId() default "";
/**
* 操作名称
* @return 操作名称
*/
String opName() default "";
/**
* 操作类型
* @return 操作类型
*/
EnumOperationRecordType recordType() default EnumOperationRecordType.DEAFULT;
/**
* 执行模式 默认异步执行
* @return 执行模式
*/
LogExecutionTypeEnum executionType() default LogExecutionTypeEnum.ASYNC;
/**
* 操作备注
* @return 操作备注
*/
String remark() default "";
/**
* 创建人ID
* @return 创建人ID
*/
String creatorId() default "";
/**
* 创建人姓名
* @return 创建人姓名
*/
String creatorName() default "";
/**
* 将请求转换为标准的日志格式
* @return 转换器
*/
Class<? extends LogConvert> converter() default LogConvert.class;
}
根据当前业务需要注解定义如上,其中有两个字段需要说明下
1.LogExecutionTypeEnum 执行模式
SYNC同步:需要和业务操作绑定同一事务
ASYNC异步:线程池异步执行,不影响核心业务操作
/**
* description 操作日志记录执行模式
* @author zhanghailang
* @date 2023/02/15 9:01
*/
@Getter
public enum LogExecutionTypeEnum {
/**
* 异步执行
*/
ASYNC("ASYNC"),
/**
* 同步执行
*/
SYNC("SYNC"),
;
private final String value;
LogExecutionTypeEnum(String value) {
this.value = value;
}
}
2.转换模式 converter() default LogConvert.class
== 拓展点 ==:解析逻辑在
OpLogRecordAop
中,可根据接口需要,从参数或者返回值中进行操作日志实体的构建,也适用复杂逻辑的操作日志的构建。
/**
* 操作日志记录转换器
* 实现类以Log开头 Converter结尾
* example
* @see LogAddRecordConverter
* @author hailang.zhang
* @since 2023-02-14
*/
public interface LogConvert<T, R> {
/**
* 接口执行前转换
* @param value 一般为请求参数
* @return 操作日志
*/
GigOperationRecordDO beforeConvert(T value);
/**
* 接口执行后转换
* @param value 一般为返回结果
* @param param 操作日志
* @return 操作日志
*/
GigOperationRecordDO afterConvert(R value, GigOperationRecordDO param);
}
注解的这个属性设计主要用于较为复杂的日志实体转换,如果方法的入参字段不符合日志实体字段,比如字段名称不一致,或者日志数据需要根据入参字段来查询业务数据,例如下图需要查询审核结果的状态来进行日志记录的完善
切面逻辑
OpLogRecordAop
/**
* 操作日记记录切面
* @author hailang.zhang
* @since 2023-02-14
*/
@Component
@Aspect
@Slf4j
public class OpLogRecordAop {
@Resource
private OperationRecordGateway operationRecordGateway;
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
1,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new NamedThreadFactory("log_record_thread_", false));
@Around("@annotation(opLogRecord)")
public Object around(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord) throws Throwable {
//实际方法执行
Object proceed = joinPoint.proceed();
//同步执行日志记录
if (opLogRecord.executionType() == LogExecutionTypeEnum.SYNC) {
this.sync(joinPoint, opLogRecord, proceed);
}
//异步执行日志记录
if (opLogRecord.executionType() == LogExecutionTypeEnum.ASYNC) {
this.async(joinPoint, opLogRecord, proceed);
}
return proceed;
}
/**
* 同步执行
* @param joinPoint 切面
* @param opLogRecord 配置数据
* @param proceed 执行结果
* @throws Throwable 异常
*/
private void sync(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) throws Throwable {
execute(joinPoint, opLogRecord, proceed);
throw new RuntimeException("test1111");
}
/**
* 异步执行
* @param joinPoint 切面
* @param opLogRecord 配置数据
* @param proceed 执行结果
* @throws Throwable 异常
*/
private void async(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) {
//异步执行
EXECUTOR.execute(() -> {
try {
execute(joinPoint, opLogRecord, proceed);
} catch (Throwable e) {
log.error("record log failed: {}", ExceptionUtil.stacktraceToString(e));
}
});
}
/**
* 执行
* @param joinPoint 切面
* @param opLogRecord 配置数据
* @param proceed 执行结果
* @throws Throwable 异常
*/
private void execute(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) throws Throwable {
GigOperationRecordDO opRecord = createLogRecord(joinPoint, opLogRecord, proceed);
this.operationRecordGateway.saveOperationRecord(opRecord);
}
/**
* 创建操作日志实体 两种模式
* 1:从注解数据中解析spel表达式获取日志实体
* 2:从LogConvert实现类中获取日志实体
* @param joinPoint 切面
* @param opLogRecord 配置数据
* @param proceed 执行结果
* @return 操作日志实体
* @throws Throwable 异常
*/
private GigOperationRecordDO createLogRecord(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) throws Throwable {
Class<? extends LogConvert> converter = opLogRecord.converter();
Object arg = joinPoint.getArgs()[0];
if (converter == LogConvert.class) {
//模式1:从注解数据中解析spel表达式获取日志实体
return createByParseSpelExpression(opLogRecord, arg);
} else {
//模式2:从LogConvert实现类中获取日志实体
LogConvert logConvert = null;
try {
//如果是被Spring管理的Bean
logConvert = SpringUtil.getBean(opLogRecord.converter());
} catch (Exception e) {
log.info("log bean not find");
}
if (logConvert == null) {
logConvert = converter.newInstance();
}
GigOperationRecordDO result = logConvert.beforeConvert(arg);
result = logConvert.afterConvert(proceed, result);
return result;
}
}
/**
* 从注解数据中解析表达式获取日志实体
* @param opLogRecord 注解配置数据
* @param arg 方法请求参数
* @return 日志实体
*/
private GigOperationRecordDO createByParseSpelExpression(OpLogRecord opLogRecord, Object arg) {
GigOperationRecordDO result = new GigOperationRecordDO();
SpelExpressionParser parser = new SpelExpressionParser();
result.setBizId((String) parser.parseExpression(opLogRecord.bizId(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
result.setCreatorId((Long) parser.parseExpression(opLogRecord.creatorId(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
result.setCreatorName((String) parser.parseExpression(opLogRecord.creatorName(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
result.setRecordType(opLogRecord.recordType().getValue());
result.setRemark((String) parser.parseExpression(opLogRecord.remark(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
result.setOpName(opLogRecord.opName());
result.setCreateTime(Instant.now());
return result;
}
}
实际业务注解使用
converter模式
比较简单直接加上注解,定义实体转换的converter即可
spel表达式模式
需要把构建日志需要的字段,在注解的属性上一一填写