在我们日常开发系统中,甚至在一些软件标书中,都会有一项,即需要将每个用户的操作记录下来,支持对用户的操作、配置等有关动作进行日志记录,保证系统操作使用有记录可寻。
本文针对这一功能,采用spring aop 外加java反射机制即Spring反射工具ReflectionUtils来动态实现删除前的数据获取,只需定义一个注解,就能完成所有日志的记录,日志表采用按照每天进行分表记录,全程详解操作日志的记录,相信看完本篇文章后,每个人能够根据自己的业务完成操作日志的记录功能的开发。
分表记录日志
项目采用springboot+postgreSql,对于分表进行记录的话,目前有三种方式可实现分表记录查询:Sharding Jdbc、定时任务、以及pg自带的分表功能。简单期间,本文采用定时任务的方式记录分表操作,即日志记录表在代码中创建。
DynamicSQLUtil类动态判断日志表存在以及动态创建日志表。
@Slf4j
public class DynamicSQLUtil {
/**
* 通过表名查看表是否存在
*
* @param tableName 表名
* @return SQL
*/
public String tableExists(String tableName) {
return "select count(*) from pg_class where relname = '" + tableName + "'";
}
/**
* 按天创建日志信息表
* ConstantDefine.LOG_INFO_TABLE_NAME为"db_log_"
* @return SQL
*/
public String createTableLogInfo(String currentDate) {
String tableName = ConstantDefine.LOG_INFO_TABLE_NAME + currentDate;
return "create table if not exists " + tableName +
"(\n" +
"\tid serial not null\n" +
"\t\tconstraint db_log_pkey" + currentDate +
"\t\t\tprimary key,\n" +
"\taccount varchar(100) not null,\n" +
"\torg_code varchar(20) not null," +
"\torg_value varchar(100) not null," +
"\tip varchar(20) not null," +
"\toperation_type integer not null,\n" +
"\toperation_time timestamp not null,\n" +
"\toperation_content varchar\n" +
")\n" +
";";
}
}
可以看到日志记录表内容包括了:用户名、组织编码、组织名称、ip地址、操作时间、操作类型和操作内容等。此部分可以根据具体业务来自定义。
DblogMapper类为具体的dao层,用于实现新增日志,查询日志记录等,如下
public interface DblogMapper{
//新增日志记录
int insert(@Param("name") String tableName,
@Param("info") DbLog dbLog);
/**
* 获取记录总数
* @param tableName 表名
* @param logInfoDTO 查询条件
* @return total num
*/
int getTotal(@Param("tableName") String tableName, @Param("logInfo") DbLogInfoDTO logInfoDTO);
List<DbLog> getByPage(@Param("tableName") String tableName, @Param("pageBegin") Integer pageBegin,
@Param("pageEnd") Integer pageEnd, @Param("logInfo") DbLogInfoDTO logInfoDTO);
@SelectProvider(value = DynamicSQLUtil.class, method = "tableExists")
int exist(String tableName);
@SelectProvider(value = DynamicSQLUtil.class, method = "createTableLogInfo")
void createLogInfo(String currentDate);
实体类DbLog和DbLogInfoDTO
/**
*
* @TableName db_log
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DbLog implements Serializable {
/**
* 主键
*/
private int id;
/**
* 组织编码
*/
private String orgCode;
/**
* 组织名称
*/
private String orgValue;
/**
* 操作时间
*/
private Timestamp operationTime;
/**
* ip地址
*/
private String ip;
/**
* 操作内容
*/
private String operationContent;
/**
* 操作类型
*/
private int operationType;
/**
* 用户
*/
private String account;
private static final long serialVersionUID = 1L;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class DbLogInfoDTO extends DbLog {
private String queryDate;
private String operationTypeExplain;
private String orgCode;
private Integer page;
private Integer pageSize;
}
Service层根据日志,获动态创建日志和查询日志的封装(service接口此处省略),其中OnlineUser类为当前用户信息,可通过系统的token解析获取等。
@Service
@Slf4j
public class DblogServiceImpl implements DblogService {
@Resource
DblogMapper dblogMapper;
// 当前日期 2022-09-19 -> 20220918
static String currentDate = LocalDate.now().toString().replaceAll("-", "");
// 临时log,用于数据库表创建的缓冲期
static List<DbLog> tempLog = new LinkedList<>();
// 每日表名
static String tableName = ConstantDefine.LOG_INFO_TABLE_NAME + currentDate;
@Override
public void addLogInfo(OnlineUser onlineUser, int operatType,
String content, String ip) {
DbLog dbLog = DbLog.builder().account(onlineUser.getAccount()).operationType(operatType)
.operationTime(new Timestamp(System.currentTimeMillis()))
.orgCode(onlineUser.getOrgCode())
.orgValue(onlineUser.getRegistAgency())
.operationContent(content)
.ip(ip)
.build();
if (dblogMapper.exist(tableName) > 1) {
int result = dblogMapper.insert(ConstantDefine.LOG_INFO_TABLE_NAME + currentDate,
dbLog);
log.info("log insert result : [{}], date : [{}]", result, currentDate);
}
//分页获取日志记录
@Override
public Result getByPage(OnlineUser onlineUser, DbLogInfoDTO logInfo) {
Integer page = StructureUtil.initPage(logInfo.getPage());
Integer pageSize = StructureUtil.initPageSize(logInfo.getPageSize());
Integer pageBegin = (page - 1) * pageSize;
String tableName;
if (StringUtils.isNotBlank(logInfo.getQueryDate())) {
tableName = ConstantDefine.LOG_INFO_TABLE_NAME + logInfo.getQueryDate().replaceAll("-", "");
} else {
tableName = ConstantDefine.LOG_INFO_TABLE_NAME + currentDate;
}
if (dblogMapper.exist(tableName) < 1) {
return Result.build(MsgCodeAndDescribe.ErrorCode.NO_LOG, null);
}
if (StringUtils.isBlank(logInfo.getOrgCode())) {
logInfo.setOrgCode(onlineUser.getOrgCode())
}
List<DbLog> dbLogInfoList = dblogMapper.getByPage(tableName, pageBegin, pageSize, logInfo);
List<DbLogInfoDTO> dtoList = new ArrayList<>(dbLogInfoList.size());
dbLogInfoList.forEach(c->{
DbLogInfoDTO dto = new DbLogInfoDTO();
BeanUtils.copyProperties(c, dto);
for (DbOperation dbOperation : DbOperation.values()) {
if (dto.getOperationType() == dbOperation.getOperationType()) {
dto.setOperationTypeExplain(dbOperation.getOperationTypeExplain());
}
}
dtoList.add(dto);
});
int total = dblogMapper.getTotal(tableName, logInfo);
Result result = Result.buildPage(MsgCodeAndDescribe.ErrorCode.OK, dtoList, dtoList.size() ,total);
return result;
}
}
定时任务创建:每天凌晨创建表以及项目启动后创建表
@Slf4j
@Component
public class DblogTimer {
@Resource
DblogService dblogService;
@Resource
DblogMapper dblogMapper;
/**
* 项目启动后创建
*/
@Scheduled(initialDelay = 1000, fixedDelay = 7200000)
public void initTable() {
if (dblogMapper.exist(tableName) < 1) {
dblogMapper.createLogInfo(currentDate);
log.info("\n table:{} create sucess", ConstantDefine.LOG_INFO_TABLE_NAME + currentDate);
}
}
/**
* 每天凌晨执行建表
*/
@Scheduled(cron = "0 0 0 * * ?")
public void dailyCreateDbTable() {
currentDate = LocalDate.now().toString().replaceAll("-", "");
tableName = ConstantDefine.LOG_INFO_TABLE_NAME + currentDate;
if (dblogMapper.exist(tableName) < 1) {
dblogMapper.createLogInfo(currentDate);
}
}
}
至此,完成日志记录按天分表的操作。
利用Spring AOP和反射机制生成操作记录
采用spring aop的环绕切面的方式:@Around。
流程图如下:
首先定义切面注解AutoLog:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoLog {
/**
* 日志内容
*
* @return
*/
String value() default "";
/**
* 操作日志类型
*
* @return (1查询,2删除,3修改,4新增,5导出)
*/
int operateType() default 1;
String serviceClass() default "";
String paramMethod() default "";
}
其中serviceClass和paramMethod为在删除操作时,记录动态调度的方法!
自定义切面类AutoLogAspect
/**
* Create by yezipi
*/
@Aspect
@Component
@Slf4j
public class AutoLogAspect {
@Resource
RedisTemplate redisTemplate;
@Resource
DblogService dblogService;
@Pointcut("@annotation(com.uniview.machine.common.aspect.annotation.AutoLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around (ProceedingJoinPoint point) throws Throwable {
return saveSysLog(point);
}
private Object saveSysLog (ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String apiName = signature.getName();
Method method = signature.getMethod();
AutoLog syslog = method.getAnnotation(AutoLog.class);
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String content = syslog.value();
//如果为删除操作
if (DbOperation.DELETE.getOperationType() == syslog.operateType()) {
content = dealDeleteAop(content, syslog, joinPoint);
}
Object obj = joinPoint.proceed();
if (Result.class.isInstance(obj)) {
Result res = (Result)obj;
if (res.getCode().equals(ConstantDefine.OK)) {
content+=":" + "操作成功";
} else {
content+=":" + "操作失败:" + res.getMessage();
}
}
//获取用户信息,此处将用户信息记录到了redis中
OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(request.getHeader(ConstantDefine.TOKEN));
//获取ip地址
String ip = IPUtils.getIpAddr(request);
dblogService.addLogInfo(onlineUser, syslog.operateType(), content, ip);
return obj;
}
/**
* 利用反射机制动态获取删除前的内容
* @param content
* @param syslog
* @param joinPoint
* @return
*/
private String dealDeleteAop (String content, AutoLog syslog, ProceedingJoinPoint joinPoint) {
//此时为删除操作,获取调动参数
Object[] args = joinPoint.getArgs();
//获取自定义方法类
String serviceClass = syslog.serviceClass();
//获取删除前需要调用的方法
String paramMethod = syslog.paramMethod();
Method methodLocal;
if (null != args && args.length > 0) {
for (int i=0; i < args.length; i ++) {
Object parma = args[i];
if (parma instanceof String || parma instanceof Integer) {
//Srping反射工具ReflectionUtils获取删除前的调用方法
methodLocal = ReflectionUtils.findMethod(SpringContextUtils
.getBean(serviceClass).getClass(), paramMethod, null);
//利用spring上下文加载获取调用方法类
Object beanName = SpringContextUtils.getBean(serviceClass);
//动态调用方法
Object o = ReflectionUtils.invokeMethod(methodLocal, beanName, parma);
if (null == o) {
return content;
}
StringBuilder stringBuilder = new StringBuilder(",删除内容:【");
stringBuilder.append(o).append("】");
content+=stringBuilder.toString();
}
}
}
return content;
}
}
Controller类:
@DeleteMapping("/org/delCamera")
@AutoLog(value = "删除相机", operateType = 2, serviceClass="orgCameraMapper", paramMethod = "getCameraCodeById")
public Result delCameraForOrg(@RequestParam Integer id) {
return orgCameraService.delCameraForOrg(id);
}
可以看到此处serviceClass为一Dao层方法,我们可通过ReflectionUtils工具类动态获取serviceClass所指定的即orgCameraMapper的Bean。并且paramMethod 指定了所需调用的方法为getCameraCodeById即通过id获取相机编码,那么同样通过反射类ReflectionUtils可动态调用orgCameraMapper的getCameraCodeById类,便获取到了删除前的数据记录。
写在最后
其实ReflectionUtils工具类非常强大,springboot在加载创建bean的时候就用了此工具类,后面有时间,可以对此工具类专门探讨一下。另外,postgreSql功能也非常强大,其可自带分表功能,本文为了简单采用的是简单的每日java代码创建日期表,其实可利用pg自带的分表功能创建物理分表,后续针对pg分表我也会单独写一篇分享。