前言
今天跟大家分享的是业务日志记录的实现,主要是有些业务场景要记录变更前、变更后具体修改内容等信息,仅仅用aop已经不能满足了,其实也就是日志已经耦合业务了。
一、实现思路
思路描述
自定义一个注解,用在具体方法上,实现主动记录基础日志(AOP)。在方法里面提供业务日志工具类,工具类可以根据业务个性化修改注解实现记录的基础日志(不在方法上加注解,工具类在ThreadLocal拿不到日志信息可以直接创建)。
实现流程图
二、实现步骤
1.建表与实体
日志记录建表语句
CREATE TABLE `base_business_log` (
`id_` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键,自增',
`request_code_` varchar(30) DEFAULT NULL COMMENT '请求id',
`request_ip_` varchar(30) DEFAULT NULL COMMENT '请求ip',
`operate_privilege_id_` bigint(20) DEFAULT NULL COMMENT '操作菜单id',
`operate_privilege_name_` varchar(200) DEFAULT NULL COMMENT '操作菜单名称',
`operate_privilege_code_` varchar(200) DEFAULT NULL COMMENT '菜单编码,来源权限码',
`param_` longtext COMMENT '入参',
`core_param_` text COMMENT '核心参数描述',
`action_type_` int(11) DEFAULT NULL COMMENT '操作动作,1新增,2编辑,3删除,4关联设备。。。',
`action_type_name_` varchar(100) DEFAULT NULL COMMENT '操作动作名称',
`before_info_` longtext COMMENT '操作前信息',
`after_info_` longtext COMMENT '操作后信息',
`action_result_` varchar(50) DEFAULT NULL COMMENT '操作结果,成功success,失败fail',
`result_` bit(1) DEFAULT NULL COMMENT '操作结果 false-失败 true-成功',
`result_content_` text COMMENT '结果内容,或异常信息',
`memo_` text COMMENT '备注',
`application_id_` bigint(20) DEFAULT NULL COMMENT '应用id',
`application_name_` varchar(300) DEFAULT NULL COMMENT '应用名称',
`creator_id_` bigint(20) DEFAULT NULL COMMENT '创建人id',
`create_time_` datetime DEFAULT NULL COMMENT '创建时间',
`modify_id_` bigint(20) DEFAULT NULL COMMENT '修改人id',
`modify_time_` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_` int(11) DEFAULT '0' COMMENT '是否删除,0否,1是',
PRIMARY KEY (`id_`),
KEY `base_business_log_operate_privilege_id__IDX` (`operate_privilege_id_`) USING BTREE,
KEY `base_business_log_operate_privilege_name__IDX` (`operate_privilege_name_`) USING BTREE,
KEY `base_business_log_creator_id__IDX` (`creator_id_`) USING BTREE,
KEY `base_business_log_create_time__IDX` (`create_time_`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=59632 DEFAULT CHARSET=utf8 COMMENT='业务日志记录表';
实体
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.smartPark.common.entity.BaseEntity;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 业务日志记录表
*
* @author zhengwen
* @since 2023-04-13
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("base_business_log")
public class BusinessLog extends BaseEntity<BusinessLog> {
private static final long serialVersionUID = 1L;
/**
* 主键,自增
*/
@TableId(value = "id_", type = IdType.AUTO)
private Long id;
/**
* 请求id
*/
@TableField("request_code_")
private String requestCode;
/**
* 请求ip
*/
@TableField("request_ip_")
private String requestIp;
/**
* 操作菜单id
*/
@TableField("operate_privilege_id_")
private Long operatePrivilegeId;
/**
* 操作菜单名称
*/
@TableField("operate_privilege_name_")
private String operatePrivilegeName;
/**
* 操作菜单编码,来源于权限编码
*/
@TableField("operate_privilege_code_")
private String operatePrivilegeCode;
/**
* 参数
*/
@TableField("param_")
private String param;
/**
* 核心参数描述
*/
@TableField("core_param_")
private String coreParam;
/**
* 操作动作,1新增,2编辑,3删除,4关联设备。。。
*/
@TableField("action_type_")
private Integer actionType;
/**
* 操作动作名称
*/
@TableField("action_type_name_")
private String actionTypeName;
/**
* 操作前信息
*/
@TableField("before_info_")
private String beforeInfo;
/**
* 操作后信息
*/
@TableField("after_info_")
private String afterInfo;
/**
* 操作结果,成功success,失败fail
*/
@TableField("action_result_")
private String actionResult;
/**
* 操作结果,成功success,失败fail
*/
@TableField("result_")
private Boolean result;
/**
* 结果内容,或异常信息
*/
@TableField("result_content_")
private String resultContent;
/**
* 备注
*/
@TableField("memo_")
private String memo;
/**
* 应用id
*/
@TableField("application_id_")
private Long applicationId;
/**
* 应用名称
*/
@TableField("application_name_")
private String applicationName;
/**
* 创建人id
*/
@TableField("creator_id_")
private Long creatorId;
/**
* 创建时间
*/
@TableField("create_time_")
private Date createTime;
/**
* 修改人id
*/
@TableField("modify_id_")
private Long modifyId;
/**
* 修改时间
*/
@TableField("modify_time_")
private Date modifyTime;
/**
* 是否删除,0否,1是
*/
@TableLogic(value = "0", delval = "1")
@TableField("deleted_")
private Integer deleted;
}
2.核心代码
自定义注解
import com.xxx.LogConstant;
import java.lang.annotation.*;
/**
* 操作日志注解
*
* @author zhengwen
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BusinessLogAnnotate {
/**
* 是否记录参数 默认 true
* @return
*/
boolean recordParam() default true;
/**
* 操作动作
* @return
*/
LogConstant.LogOperateActionType actionType();
/**
* 描述
* @return
*/
String desc();
/**
* 操作菜单编码
* @return
*/
String menuCode();
}
业务操作常量类
/**
* 日志管理常量
*
* @author zhengwen
*/
public interface LogConstant {
/**
* 日志操作动作
*/
enum LogOperateActionType {
/**
* 操作动作
*/
ADD(1, "新增"),
EDIT(2, "修改"),
DEL(3, "删除"),
QUERY(4, "查询"),
DEVICE_CONTROL(5,"设备控制" ),
DEVICE_TACTIC(6,"设备操作策略" ),
DEVICE_PLAN(7,"设备操作计划" ),
ALARM_CONFIG(8,"配置" ),
APPROVE(9,"审核" ),
JOIN_DEVICE(10,"关联设备" ),
ENABLE(11,"启用" ),
DISABLE(12,"禁用" ),
EXPORT(13,"导出" ),
IMPORT(14,"导入" ),
;
Integer actionType;
/**
* 名称
*/
private String actionTypeName;
public Integer getActionType() {
return actionType;
}
public String getActionTypeName() {
return actionTypeName;
}
LogOperateActionType(Integer actionType, String actionTypeName) {
this.actionType = actionType;
this.actionTypeName = actionTypeName;
}
}
}
日志工具类
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.xx.BusinessLog;
import com.xx.LogConstant;
import com.xx.BaseApplication;
import com.xx.BaseappUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import site.morn.translate.Translator;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Date;
@Component
@Slf4j
public class LogHelper {
private static Translator translator;
private LogHelper() {
}
private static final ThreadLocal<String> logContentThreadLocal = new ThreadLocal<>();
private static final ThreadLocal<BaseappUser> customerThreadLocal = new ThreadLocal<>();
private static final ThreadLocal<BaseApplication> applicationThreadLocal = new ThreadLocal<>();
private static final ThreadLocal<BusinessLog> businessLogThreadLocal = new ThreadLocal<>();
public static BusinessLog getBusinessLog() {
return businessLogThreadLocal.get();
}
public static void setBusinessLog(BusinessLog businessLog) {
businessLogThreadLocal.set(businessLog);
}
public static BaseApplication getApplication() {
return applicationThreadLocal.get();
}
public static void setApplication(BaseApplication application) {
applicationThreadLocal.set(application);
}
public static String getContent() {
return logContentThreadLocal.get();
}
public static void setContent(String content, Object... args) {
if (null == translator) {
translator = SpringUtil.getBean(Translator.class);
}
String message = translator.translate(content, args, content);
logContentThreadLocal.set(message);
}
public static BaseappUser getCustomer() {
return customerThreadLocal.get();
}
public static void setCustomer(BaseappUser customer) {
customerThreadLocal.set(customer);
}
public static void clearContent() {
logContentThreadLocal.remove();
customerThreadLocal.remove();
applicationThreadLocal.remove();
businessLogThreadLocal.remove();
}
public static String beanToLog(Object obj) {
return beanToLog(obj, true);
}
public static String beanToLog(Object obj, boolean ignoreNull) {
StringBuilder sb = new StringBuilder();
try {
if (null != obj) {
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field field : declaredFields) {
LogField f = field.getAnnotation(LogField.class);
if (f == null) {
continue;
}
String prefix = "get";
if (field.getType() == boolean.class) {
prefix = "is";
}
String methodName = prefix + StringUtils.capitalize(field.getName());
Method method = ReflectUtil.getMethod(obj.getClass(), methodName);
if (method == null) {
continue;
}
Object value = ReflectUtil.invoke(obj, method);
if (ignoreNull && value == null) {
continue;
}
if (value instanceof Date) {
value = DateUtil.formatDateTime((Date) value);
}
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(f.name()).append(":").append(value);
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return sb.toString();
}
/**
* 设置菜单编码
*
* @param privilegeCode 菜单编码
*/
public static void setMenuCode(String privilegeCode) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
businessLog.setOperatePrivilegeCode(privilegeCode);
}
}
/**
* 设置核心参数
*
* @param coreParam 核心参数
*/
public static void setCoreParam(String coreParam) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
businessLog.setCoreParam(coreParam);
}
}
/**
* 设置操作前信息
*
* @param beforeInfoJson 操作前信息json串
*/
public static void setBeforeInfo(String beforeInfoJson) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
businessLog.setBeforeInfo(beforeInfoJson);
}
}
/**
* 设置操作后信息
*
* @param afterInfoJson 操作后信息json串
*/
public static void setAfterInfo(String afterInfoJson) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
businessLog.setAfterInfo(afterInfoJson);
}
}
/**
* 设置描述
*
* @param desc 描述
*/
public static void setDesc(String desc) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
businessLog.setMemo(desc);
}
}
public static void setLogInfo(String menuCode, String coreParam) {
setLogInfo(menuCode, coreParam, null);
}
public static void setLogInfo(String menuCode, String coreParam, String beforeInfoJsonStr) {
setLogInfo(menuCode, coreParam, beforeInfoJsonStr, null);
}
public static void setLogInfo(String menuCode, String coreParam, String beforeInfoJsonStr, String afterInfoJsonStr) {
setLogInfo(menuCode, coreParam, beforeInfoJsonStr, afterInfoJsonStr, null);
}
public static void setLogInfo(String menuCode, String coreParam, String beforeInfoJsonStr, String afterInfoJsonStr, String descStr) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
if (StringUtils.isNotBlank(menuCode)) {
businessLog.setOperatePrivilegeCode(menuCode);
}
if (StringUtils.isNotBlank(coreParam)) {
businessLog.setCoreParam(coreParam);
}
if (StringUtils.isNotBlank(beforeInfoJsonStr)) {
businessLog.setBeforeInfo(beforeInfoJsonStr);
}
if (StringUtils.isNotBlank(afterInfoJsonStr)) {
businessLog.setAfterInfo(afterInfoJsonStr);
}
if (StringUtils.isNotBlank(descStr)){
if(descStr.length() >= 4000){
descStr = descStr.substring(0, 3500) + "...";
}
businessLog.setMemo(descStr);
}
}
}
/**
* 记录日志
* @param menuCode 菜单code
* @param actionType 动作枚举
* @param descStr 描述
*/
public static void setLogInfo(String menuCode, LogConstant.LogOperateActionType actionType, String descStr) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
if (StringUtils.isNotBlank(menuCode)) {
businessLog.setOperatePrivilegeCode(menuCode);
}
if (null != actionType){
businessLog.setActionType(actionType.getActionType());
businessLog.setActionTypeName(actionType.getActionTypeName());
}
if (StringUtils.isNotBlank(descStr)){
businessLog.setMemo(descStr);
}
}
}
/**
* 记录日志描述
* @param memo 描述
*/
public static void setLogMemo(String memo) {
BusinessLog businessLog = getBusinessLog();
if (businessLog != null) {
if (StringUtils.isNotBlank(memo)){
businessLog.setMemo(memo);
}
}
}
}
AOP切面
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author zhengwen
*/
@Component
@Aspect
@Slf4j
public class BusinessLogAspect {
/**
* 环境变量
*/
private static Environment environment;
private static BusinessLogService businessLogService;
private static CommonService commonService;
private static RedisUtil redisUtil;
@PostConstruct
private void init() {
environment = SpringUtil.getBean(Environment.class);
businessLogService = SpringUtil.getBean(BusinessLogService.class);
commonService = SpringUtil.getBean(CommonService.class);
redisUtil = SpringUtil.getBean(RedisUtil.class);
}
private static int charLength = 40000;
@Pointcut("@annotation(com.smartPark.common.annotation.BusinessLogAnnotate)")
private void pointcut() {
}
@Before(argNames = "joinPoint,businessLogAnnotate", value = "pointcut() && @annotation(businessLogAnnotate)")
private void doBefore(JoinPoint joinPoint, BusinessLogAnnotate businessLogAnnotate) {
String applicationName = environment.getProperty("sanzhi.application.name");
if (StringUtils.isBlank(applicationName)) {
log.warn("未配置应用信息");
return;
}
Object[] args = joinPoint.getArgs();
if (ArrayUtils.isNotEmpty(args)) {
String paramStr = JSONUtil.toJsonStr(args);
BaseApplication baseApplication = (BaseApplication) redisUtil.hget(RedisConstant.APPLICATION, applicationName);
if (baseApplication == null) {
log.warn("未找到应用信息");
return;
}
if (businessLogAnnotate != null) {
LogConstant.LogOperateActionType logActionType = businessLogAnnotate.actionType();
BusinessLog businessLog = BusinessLog.builder()
.actionType(logActionType.getActionType())
.actionTypeName(logActionType.getActionTypeName())
.operatePrivilegeCode(businessLogAnnotate.menuCode())
.memo(businessLogAnnotate.desc())
.build();
if (baseApplication != null) {
businessLog.setApplicationId(baseApplication.getId());
businessLog.setApplicationName(baseApplication.getName());
}
if (StringUtils.isNotBlank(paramStr) && paramStr.length() < charLength) {
businessLog.setParam(paramStr);
}
LogHelper.setBusinessLog(businessLog);
}
}
}
@AfterReturning(pointcut = "pointcut() && @annotation(businessLogAnnotate)", returning = "returnObj")
public void doAfterReturning(JoinPoint joinPoint, BusinessLogAnnotate businessLogAnnotate, Object returnObj) {
String desc = businessLogAnnotate.desc();
String applicationName = environment.getProperty("sanzhi.application.name");
if (StringUtils.isBlank(applicationName)) {
log.warn("未配置应用信息");
return;
}
BaseApplication baseApplication = (BaseApplication) redisUtil.hget(RedisConstant.APPLICATION, applicationName);
if (baseApplication == null) {
log.warn("未找到应用信息");
return;
}
LogConstant.LogOperateActionType logActionType = businessLogAnnotate.actionType();
BusinessLog businessLog = LogHelper.getBusinessLog();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if(requestAttributes != null){
// 获取request
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
if (null == request) {
return;
}
businessLog.setRequestIp(request.getRemoteAddr());
}
commonService.setCreateAndModifyInfo(businessLog);
doAfterReturning(joinPoint, businessLogAnnotate, returnObj, businessLog);
LogHelper.clearContent();
}
@Async
public void doAfterReturning(JoinPoint joinPoint, BusinessLogAnnotate businessLogAnnotate, Object returnObj, BusinessLog businessLog) {
if (returnObj != null) {
String jsonStr = JSON.toJSONString(returnObj);
businessLog.setResultContent(JSON.toJSONString(returnObj));
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
Boolean isSuccess = jsonObject.getBoolean("success");
businessLog.setResult(isSuccess);
businessLog.setActionResult(jsonObject.getString("code"));
} else {
// 返回为空时默认为成功
businessLog.setResult(true);
}
addBusinessLog(joinPoint, businessLogAnnotate, businessLog);
}
@AfterThrowing(pointcut = "pointcut() && @annotation(businessLogAnnotate)", throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint, BusinessLogAnnotate businessLogAnnotate, Exception ex) {
String desc = businessLogAnnotate.desc();
String applicationName = environment.getProperty("sanzhi.application.name");
if (StringUtils.isBlank(applicationName)) {
log.warn("未配置应用信息");
return;
}
BaseApplication baseApplication = (BaseApplication) redisUtil.hget(RedisConstant.APPLICATION, applicationName);
if (baseApplication == null) {
log.warn("未找到应用信息");
return;
}
BusinessLog businessLog = LogHelper.getBusinessLog();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if(requestAttributes != null){
// 获取request
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
if (null == request) {
return;
}
}
doAfterThrowing(joinPoint, businessLogAnnotate, ex, businessLog);
LogHelper.clearContent();
}
@Async
public void doAfterThrowing(JoinPoint joinPoint, BusinessLogAnnotate businessLogAnnotate, Exception ex, BusinessLog businessLog) {
if (ex != null) {
businessLog.setResultContent("异常信息:" + ex.toString());
}
businessLog.setResult(false);
addBusinessLog(joinPoint, businessLogAnnotate, businessLog);
}
public void addBusinessLog(JoinPoint joinPoint, BusinessLogAnnotate businessLogAnnotate, BusinessLog businessLog) {
// 参数
if (businessLogAnnotate.recordParam() && joinPoint.getArgs() != null) {
Object[] args = joinPoint.getArgs();
Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.asList(args).stream();
List<Object> logArgs = stream
.filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse) && !(arg instanceof MultipartFile)))
.collect(Collectors.toList());
//过滤后序列化无异常
String string = JSON.toJSONString(logArgs);
if (StringUtils.isNotEmpty(string) && string.length() > charLength) {
try {
string = CharCompressUtil.compressWrap(string);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
businessLog.setCoreParam(string);
}
businessLogService.saveOne(businessLog);
}
}
3.使用方法
controller里
/**
* 保存广告素材
*
* @param serveDeviceVo 智能服务vo
* @return 统一出参
*/
@PostMapping("/saveAdList")
@ApiOperation("保存广告素材")
@BusinessLogAnnotate(actionType = LogConstant.LogOperateActionType.ADD, menuCode = "smartPublicToilet:smartServe:toiletAd:save", desc = "保存广告素材")
public RestMessage saveAdList(@RequestBody SmartServeDeviceVo serveDeviceVo) {
Assert.notNull(serveDeviceVo, "参数不能为空");
Assert.notNull(serveDeviceVo.getToiletDevice().getId(), "设备id不能为空");
Assert.notNull(serveDeviceVo.getAdList(), "设备素材不能为空");
return toiletDeviceAdService.saveAdList(serveDeviceVo);
}
serviceImpl里
@Override
public RestMessage saveAdList(SmartServeDeviceVo serveDeviceVo) {
StringJoiner sj = new StringJoiner(",");
ToiletDevice toiletDevice = serveDeviceVo.getToiletDevice();
Long deviceId = toiletDevice.getId();
ToiletDevice dbToiletDevice = toiletDeviceMapper.selectById(deviceId);
sj.add("保存广告素材,设备编码:" + dbToiletDevice.getDeviceCode());
sj.add("设备名称:" + toiletDevice.getDeviceName());
//先删除广告素材
QueryWrapper<ToiletDeviceAd> qw = new QueryWrapper<>();
qw.eq("toilet_device_id_", deviceId);
qw.eq("deleted_", CommonConstant.NOT_DELETE);
baseMapper.delete(qw);
//再新增新素材
List<ToiletDeviceAd> adList = serveDeviceVo.getAdList();
if (CollectionUtil.isNotEmpty(adList)) {
sj.add("素材url地址:");
adList.forEach(ad -> {
ad.setToiletDeviceId(deviceId);
commonService.setCreateAndModifyInfo(ad);
sj.add(ad.getAdFileUrl());
baseMapper.insert(ad);
});
}
LogHelper.setLogInfo(null, null, null, null, sj.toString());
//TODO 是否要处理素材下发?
return RestBuilders.successBuilder().build();
}
LogHelper.setLogInfo方法有几个传参的方法。其实我觉得这个工具类提供的不是蛮好,还可以优化。
总结
- 自定义操作业务日志就这么简单
- 业务操作记录到变更字段避免不了跟业务耦合
- 其实还有一种方法是跟每张表建一张影子表,带版本号,通过版本找历史
好,就写到这里,希望可以帮到大家,uping!