【SpringAOP+反射机制】实现操作日志记录详解

发布于:2022-12-13 ⋅ 阅读:(245) ⋅ 点赞:(0)

            在我们日常开发系统中,甚至在一些软件标书中,都会有一项,即需要将每个用户的操作记录下来,支持对用户的操作、配置等有关动作进行日志记录,保证系统操作使用有记录可寻。

        本文针对这一功能,采用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分表我也会单独写一篇分享。


网站公告

今日签到

点亮在社区的每一天
去签到