基于方法的日志注解,持久化入参回参,异常通知,收集traceId。
数据库设计:
CREATE TABLE `s_api_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`biz_category` varchar(50) DEFAULT NULL COMMENT '业务目录,默认类名',
`biz_type` varchar(50) DEFAULT NULL COMMENT '业务类型,默认方法名',
`biz_no` varchar(50) DEFAULT NULL COMMENT '业务号,支持el表达式',
`exe_res` tinyint(1) DEFAULT NULL COMMENT '结果',
`sub_biz_no` varchar(50) DEFAULT NULL COMMENT '子业务号',
`op_params` varchar(255) DEFAULT NULL COMMENT '参数',
`trace_id` varchar(20) DEFAULT NULL COMMENT 'traceId',
`execution_result` varchar(255) DEFAULT NULL COMMENT '执行结果\n',
`err_msg` varchar(255) DEFAULT NULL COMMENT '异常内容\n',
`operator_name` varchar(20) DEFAULT NULL COMMENT '操作人\n',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'createTime',
PRIMARY KEY (`id`),
KEY `bizCategory_bizType_bizNo` (`biz_category`,`biz_type`,`biz_no`) USING BTREE
) DEFAULT CHARSET=utf8mb4 COMMENT='接口日志表';
方案设计:
aop处理,环绕增强,同步解析出Dao层数据结构,将数据传入自定义处理器。
//主启动类配置自定义处理器和加载aop
@ApiLogRecordConfiguration(ApiLogRecordRabbitmqHandle.class)
@PostMapping("/xxx")
//日志注解,字段映射支持el表达式
@EnableApiLogRecord(bizType = "xxx", bizNo = "#aaa", subBizNo = "#ddd.ttt")
public ResultHolder> xxx(@RequestHeader(bbb) Long ccc,@RequestBody @Valid ddd eee){
return null;
}
一:自定义处理器接口及实现案例(推送rabbitmq消息处理)
public interface LogRecordHandle {
/**
* 日志处理
*
* @param logRecordVO vo
*/
void logHandle(ApiLogRecordVO logRecordVO);
}
public class ApiLogRecordRabbitmqHandle extends AbstractApiLogRecordHandle implements LogRecordHandle {
@Resource
private AmqpTemplate amqpTemplate;
@Override
public void logHandle(ApiLogRecordVO logRecordVO) {
//发送消息
amqpTemplate.convertAndSend(
SupplierCommonConstants.API_LOG_RECORD_EXCHANGE,
null,
JSON.toJSONString(logRecordVO));
}
}
数据实体类:
@Data
public class ApiLogRecordVO {
@ApiModelProperty(value = "主键")
private Long id;
@ApiModelProperty(value = "业务目录,默认类名")
private String bizCategory;
@ApiModelProperty(value = "业务类型,默认方法名")
private String bizType;
@ApiModelProperty(value = "业务号,支持el表达式")
private String bizNo;
@ApiModelProperty(value = "子业务号,支持el表达式")
private String subBizNo;
@ApiModelProperty(value = "执行结果,1:成功,0:失败")
private Integer exeRes;
@ApiModelProperty(value = "参数")
private String opParams;
@ApiModelProperty(value = "traceId")
private String traceId;
@ApiModelProperty(value = "执行结果")
private String executionResult;
@ApiModelProperty(value = "异常内容")
private String errMsg;
@ApiModelProperty(value = "操作人")
private String operatorName;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
@ApiModelProperty(value = "是否记录结果")
private boolean recordResult;
@ApiModelProperty(value = "指定异常通知地址")
private String webHookUrl;
}
二:webHook处理
@ConfigurationProperties(prefix = "c3.webhook.config")
@Component
@Data
public class ApiLogWebHookConfig {
/**
*webHook配置.
*/
private Map webHookConfigMap = new HashMap<>();
}
企业微信机器人通知实体类:
@Data
public class QwWebHookContent {
/**
* 默认值:text.
*/
private String msgtype;
/**
* 默认值:text.
*/
private Text Text;
/**
* 默认值:报警类型,用于匹配url.
*/
private String type;
/**
* toJsonString.
*
* @return jsonString.
*/
public String toJsonString() {
return JSON.toJSONString(this);
}
@Data
@AllArgsConstructor
public class Text {
/**
* 默认值:报警类型,用于匹配url.
*/
private String content;
private String[] mentioned_list;
private String[] mentioned_mobile_list;
/**
* content构造.
*/
public Text(String content) {
this.content = content;
}
}
}
三:自定义dao层数据转换器
1,接口:
public interface LogConvertInterface {
void convertHandle(Map paramsMap, ApiLogRecordVO apiLogRecordVO);
}
2,抽象类:
public abstract class AbstractLogConvert implements LogConvertInterface {
/**
* 处理
*/
@Override
public void convertHandle(Map paramsMap, ApiLogRecordVO apiLogRecordVO) {
}
/**
* 默认无
*/
public abstract static class None extends AbstractLogConvert {
/**
* 构造
*/
private None() {
}
}
}
四:注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface EnableApiLogRecord {
/**
* 异常通知
*
* @return webHookUrl
*/
String webHookUrl() default "";
/**
* 业务id,默认方法名,支持el表达式
*
* @return bizNo
*/
String bizNo() default "";
/**
* 业务id,默认方法名,支持el表达式
*
* @return subBizNo
*/
String subBizNo() default "";
/**
* 业务目录,默认类名,支持el表达式
*
* @return bizCategory
*/
String bizCategory() default "";
/**
* 自定义类型转换class
*
* @return webHookUrl
*/
Class extends AbstractLogConvert> logConvert() default AbstractLogConvert.None.class;
/**
* 业务id,默认方法名,支持el表达式
*
* @return bizType
*/
String bizType() default "";
/**
* 操作人,默认取参数中的username字段,支持el表达式
*
* @return username
*/
String username() default "";
/**
* 是否保存结果
*
* @return recordResult
*/
boolean recordResult() default false;
}
配置注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ApiLogRecordHandleRegistrar.class)
@EnableConfigurationProperties({ApiLogWebHookConfig.class})
public @interface ApiLogRecordConfiguration {
/**
* 日志处理方式
*
* @return value
*/
Class extends LogRecordHandle> value();
}
五:配置加载bean
public class ApiLogRecordHandleRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
registerDefaultConfiguration(importingClassMetadata, registry);
registerAop(importingClassMetadata, registry);
}
/**
* 注册配置
*
* @param importingClassMetadata metadata
* @param registry registry
*/
private void registerDefaultConfiguration(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map defaultAttrs = importingClassMetadata
.getAnnotationAttributes(ApiLogRecordConfiguration.class.getName(), false);
if (defaultAttrs != null && defaultAttrs.containsKey("value")) {
registerLogHandelConfiguration(registry, "LogRecordHandle",
(Class>) defaultAttrs.get("value"));
}
}
/**
* 注册handle
*
* @param registry registry
* @param name name
* @param logHandle logHandle
*/
private void registerLogHandelConfiguration(BeanDefinitionRegistry registry, String name, Class> logHandle) {
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(logHandle);
registry.registerBeanDefinition(name, rootBeanDefinition);
}
/**
* 注册handle
*
* @param importingClassMetadata meta
* @param registry registry
*/
private void registerAop(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(ApiLogRecordAop.class);
registry.registerBeanDefinition(ApiLogRecordAop.class.getSimpleName(), builder.getBeanDefinition());
}
}
六:aop处理
@Aspect
@Order(1)
public class ApiLogRecordAop extends ApplicationObjectSupport {
@Resource
private LogRecordHandle logRecordHandle;
@Resource
private ApiLogWebHookConfig apiLogWebHookConfig;
private static final Integer RESULT_STR_MAX = 250;
private static final Integer RESULT_STR_MIN = 0;
private static final Integer EXE_SUCCESS = 1;
private static final Integer EXE_FAIL = 0;
private static final Integer HASH_MAP_INIT = 16;
private static final String DEFAULT_STR = "default";
/**
* 切点
*/
@Pointcut("@annotation(com.jubaozan.c3.suppliercommon.logrecord.annotation.EnableApiLogRecord)")
public void logRecordPointcut() {
}
/**
* 增强
*
* @param point point
* @return Object Object
*/
@Around("logRecordPointcut()")
public Object interceptor(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
ApiLogRecordVO apiLogRecordVO = new ApiLogRecordVO();
//解析注解组装参数
parseAnnotation(apiLogRecordVO, method, point);
//执行
try {
Object res = point.proceed();
if (apiLogRecordVO.isRecordResult()) {
apiLogRecordVO.setExecutionResult(JSON.toJSONString(res).substring(RESULT_STR_MIN, RESULT_STR_MAX));
}
return res;
} catch (Throwable throwable) {
apiLogRecordVO.setExeRes(EXE_FAIL);
apiLogRecordVO.setErrMsg(throwable.getMessage());
throw new RuntimeException(throwable.getMessage(), throwable);
} finally {
//执行处理方式
try {
logRecordHandle.logHandle(apiLogRecordVO);
webHookHandle(apiLogRecordVO);
} catch (Exception e) {
logger.error("日志处理失败,e:", e);
}
}
}
/**
* 解析注解
*
* @param apiLogRecordVO apiLogRecordVO
* @param method pjp
* @param pjp pjp
*/
private void parseAnnotation(ApiLogRecordVO apiLogRecordVO, Method method, ProceedingJoinPoint pjp) {
try {
apiLogRecordVO.setTraceId(MDC.get("traceId"));
apiLogRecordVO.setExeRes(EXE_SUCCESS);
EnableApiLogRecord annotation = method.getAnnotation(EnableApiLogRecord.class);
Object[] args = pjp.getArgs();
LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = localVariableTable.getParameterNames(method);
Map paramsMap = new HashMap<>(HASH_MAP_INIT);
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < Objects.requireNonNull(paraNameArr).length; i++) {
context.setVariable(paraNameArr[i], args[i]);
paramsMap.put(paraNameArr[i], args[i]);
}
//是否指定自定义转换类
Class extends AbstractLogConvert> convertClass = annotation.logConvert();
if (convertClass != AbstractLogConvert.None.class) {
convertClass.newInstance().convertHandle(paramsMap, apiLogRecordVO);
return;
}
String bizCategory = ObjectUtil.isNotEmpty(annotation.bizCategory()) ? annotation.bizCategory()
: ((MethodInvocationProceedingJoinPoint) pjp).getSignature().getDeclaringType().getSimpleName();
if (bizCategory.matches("^#.*.$")) {
bizCategory = parser.parseExpression(bizCategory).getValue(context, String.class);
}
String bizType = ObjectUtil.isNotEmpty(annotation.bizType()) ? annotation.bizType() : method.getName();
if (bizType.matches("^#.*.$")) {
bizType = parser.parseExpression(bizType).getValue(context, String.class);
}
String bizNo = ObjectUtil.isNotEmpty(annotation.bizNo()) ? annotation.bizNo() : method.getName();
if (bizNo.matches("^#.*.$")) {
bizNo = parser.parseExpression(bizNo).getValue(context, String.class);
}
String userName = ObjectUtil.isNotEmpty(annotation.username()) ? annotation.username() : "#username";
if (userName.matches("^#.*.$")) {
userName = parser.parseExpression(userName).getValue(context, String.class);
}
String subBizNo = ObjectUtil.isNotEmpty(annotation.subBizNo()) ? annotation.subBizNo() : method.getName();
if (subBizNo.matches("^#.*.$")) {
subBizNo = parser.parseExpression(subBizNo).getValue(context, String.class);
}
apiLogRecordVO.setBizCategory(bizCategory);
apiLogRecordVO.setBizType(bizType);
apiLogRecordVO.setBizNo(bizNo);
apiLogRecordVO.setSubBizNo(subBizNo);
apiLogRecordVO.setOperatorName(userName);
apiLogRecordVO.setOpParams(JSON.toJSONString(paramsMap));
apiLogRecordVO.setRecordResult(annotation.recordResult());
apiLogRecordVO.setWebHookUrl(annotation.webHookUrl());
Optional.ofNullable(paramsMap.get(KeyConstants.KEY_X_C3_USERNAME)).map(Object::toString).ifPresent(
apiLogRecordVO::setOperatorName);
} catch (Exception ex) {
logger.error("日志数据解析失败", ex);
}
}
/**
* webHook处理
*
* @param apiLogRecordVO apiLogRecordVO
*/
private void webHookHandle(ApiLogRecordVO apiLogRecordVO) {
try {
if (apiLogRecordVO.getExeRes().equals(EXE_FAIL)) {
String url = apiLogRecordVO.getWebHookUrl();
if (ObjectUtil.isEmpty(url)) {
url = apiLogWebHookConfig.getWebHookConfigMap()
.getOrDefault(apiLogRecordVO.getBizType(), apiLogWebHookConfig.getWebHookConfigMap().get(DEFAULT_STR));
}
if (ObjectUtil.isNotEmpty(url)) {
QwWebHookContent qwHookContent = new QwWebHookContent();
qwHookContent.setMsgtype("text");
qwHookContent.setType("default");
qwHookContent.setText(qwHookContent.new Text("接口异常" + ":" + "\n"
+ "业务目录: " + apiLogRecordVO.getBizCategory() + "\n"
+ "业务类型: " + apiLogRecordVO.getBizType() + "\n"
+ "时间:" + LocalDateTime.now() + "\n"
+ "traceId: " + TraceUtils.getTraceId() + "\n"
+ "异常类容: " + apiLogRecordVO.getErrMsg()));
HttpUtil.post(url, JSON.toJSONString(qwHookContent));
}
}
} catch (Exception ex) {
logger.error("企微消息提醒异常,内容{}", ex);
}
}
}
欢迎提不足之处,和优化点