【xxl-job源码篇03】xxl-job日志系统源码解读

导读

xxl-job不光有自己的rpc,注册中心,还有自己的日志系统,可谓是麻雀虽小,五脏俱全。

本章将会从源码层面剖析xxl-job的日志系统,我们在使用xxl-job记录日志时只需要在任务执行过程中使用XxlJobHelper.log()即可,方法和log4j/slf4j一样简单。我们可以通过控制台看到该任务运行时的实时日志。

XxlJobHelper.log("hello world");

image-20220413192926220

这可太高级了,可以把日志精确到任务级别,而且还可以在控制台实时查看,调度方法,类信息该有的信息都有,那么它到底咋实现的呢,接下来将带大家剖析xxl-job的实现方式。

源码剖析

这次我们直接一点,正向来推逻日志系统逻辑,定位到log方法

com.xxl.job.core.context.XxlJobHelper#log(java.lang.String, java.lang.Object...)

public static boolean log(String appendLogPattern, Object ... appendLogArguments) {
    
    
    // 使用slf4j解析器格式化日志内容
    FormattingTuple ft = MessageFormatter.arrayFormat(appendLogPattern, appendLogArguments);
    String appendLog = ft.getMessage();
    // 获得栈帧信息
    StackTraceElement callInfo = new Throwable().getStackTrace()[1];
    return logDetail(callInfo, appendLog);
}

注意这段StackTraceElement callInfo = new Throwable().getStackTrace()[1];

这是获得调用栈帧方法,索引0为当前栈帧,1为调用栈帧,以此类推,此处获得的是索引1,也就是说获得的是调用该方法的栈帧信息,可以通过StackTraceElement获得调用类名,方法名,行数等信息

继续进入到com.xxl.job.core.context.XxlJobHelper#logDetail

private static boolean logDetail(StackTraceElement callInfo, String appendLog) {
    
    
    // 获得当前上下文对象
    XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();
    if (xxlJobContext == null) {
    
    
        return false;
    }
    // 拼接格式化日志信息
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(DateUtil.formatDateTime(new Date())).append(" ")
            .append("["+ callInfo.getClassName() + "#" + callInfo.getMethodName() +"]").append("-")
            .append("["+ callInfo.getLineNumber() +"]").append("-")
            .append("["+ Thread.currentThread().getName() +"]").append(" ")
            .append(appendLog!=null?appendLog:"");
    String formatAppendLog = stringBuffer.toString();
    // 获得日志文件路径
    String logFileName = xxlJobContext.getJobLogFileName();
    if (logFileName!=null && logFileName.trim().length()>0) {
    
    
        // 流的形式将日志写入本地文件
        XxlJobFileAppender.appendLog(logFileName, formatAppendLog);
        return true;
    } else {
    
    
        logger.info(">>>>>>>>>>> {}", formatAppendLog);
        return false;
    }
}

此处有一段关键代码

XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();

这是获得当前任务的上下文对象,在被调度的时候会被初始化,我前面文章[【xxl-job源码篇01】xxl-job源码解读 神奇的时间轮 触发流程解读]有讲过,这里不展开

我们可以看到,xxl-job将日志都写入了本地文件并没有推送给服务端,此处日志并非使用的是推的模式,而是使用的拉模式,当我们后台打开任务日志时,服务端会到客户端来拉取日志。

定位到接口com.xxl.job.admin.controller.JobLogController#logDetailCat

@RequestMapping("/logDetailCat")
@ResponseBody
public ReturnT<LogResult> logDetailCat(String executorAddress, long triggerTime, long logId, int fromLineNum){
    
    
   try {
    
    
      // 获得执行机的remote实例
      ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(executorAddress);
      // 调用客户端netty http接口拉取日志
      ReturnT<LogResult> logResult = executorBiz.log(new LogParam(triggerTime, logId, fromLineNum));
      // 判断日志是否结束
           if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) {
    
    
               XxlJobLog jobLog = xxlJobLogDao.load(logId);
               if (jobLog.getHandleCode() > 0) {
    
    
                   logResult.getContent().setEnd(true);
               }
           }
      return logResult;
   } catch (Exception e) {
    
    
      logger.error(e.getMessage(), e);
      return new ReturnT<LogResult>(ReturnT.FAIL_CODE, e.getMessage());
   }
}

当执行状态属于未完成的情况下,xxl-job日志控制台会循环调用该接口直至任务完成。

接下来看看客户端读取日志文件相关代码com.xxl.job.core.biz.impl.ExecutorBizImpl#log

@Override
public ReturnT<LogResult> log(LogParam logParam) {
    
    
    // log filename: logPath/yyyy-MM-dd/9999.log
    // 获得日志文件名
    String logFileName = XxlJobFileAppender.makeLogFileName(new Date(logParam.getLogDateTim()), logParam.getLogId());
    // 读取流信息
    LogResult logResult = XxlJobFileAppender.readLog(logFileName, logParam.getFromLineNum());
    return new ReturnT<LogResult>(logResult);
}

com.xxl.job.core.log.XxlJobFileAppender#readLog

public static LogResult readLog(String logFileName, int fromLineNum){
    
    
   if (logFileName==null || logFileName.trim().length()==0) {
    
    
           return new LogResult(fromLineNum, 0, "readLog fail, logFile not found", true);
   }
   File logFile = new File(logFileName);
   if (!logFile.exists()) {
    
    
           return new LogResult(fromLineNum, 0, "readLog fail, logFile not exists", true);
   }
   // read file
   StringBuffer logContentBuffer = new StringBuffer();
   int toLineNum = 0;
   LineNumberReader reader = null;
   try {
    
    
      reader = new LineNumberReader(new InputStreamReader(new FileInputStream(logFile), "utf-8"));
      String line = null;
      while ((line = reader.readLine())!=null) {
    
    
         toLineNum = reader.getLineNumber();       // [from, to], start as 1
         if (toLineNum >= fromLineNum) {
    
    
            logContentBuffer.append(line).append("\n");
         }
      }
   } catch (IOException e) {
    
    
      logger.error(e.getMessage(), e);
   } finally {
    
    
      if (reader != null) {
    
    
         try {
    
    
            reader.close();
         } catch (IOException e) {
    
    
            logger.error(e.getMessage(), e);
         }
      }
   }
   LogResult logResult = new LogResult(fromLineNum, toLineNum, logContentBuffer.toString(), false);
   return logResult;
}

xxl-job客户端会将日志文件存入磁盘中,当打开日志控制台时,服务端会循环请求客户端,客户端通过io流的形式把指定行数的日志读取出来并返回给服务端

链接

【xxl-job源码篇01】xxl-job源码解读 神奇的时间轮 触发流程解读

【xxl-job源码篇02】注册中心 自研RPC netty的应用?

猜你喜欢

转载自blog.csdn.net/qq_21046665/article/details/124166873