零基础鸿蒙应用开发第三十五节:高级日志工具hilog封装

零基础鸿蒙应用开发学习计划表

【学习目标】

  1. 掌握hilog与console的本质区别,明确鸿蒙应用开发环境下日志工具的选型标准与开发规范。
  2. 熟练运用hilog分级日志,结合模块化开发需求,实现日志的分类管理与高效筛选。
  3. 独立实现可直接落地的HiLogUtil工具类,支持双模式兼容(可输入/可不输入%d/%s/%b/%o/%n占位符),同时覆盖长日志分段、敏感数据保护、环境开关等核心场景。
  4. 掌握项目日志体系迁移技巧,高效替换console,提升开发调试与问题排查效率。
  5. 强化静态方法、函数重载、静态属性、正则表达式等ArkTS/TypeScript核心知识点的实战应用能力。

【学习重点】

  1. 工具类封装逻辑:通过interface+私有构造器实现结构化数据处理,新增占位符检测逻辑实现双模式分支处理。
  2. 核心功能实现:
    • 占位符模式:兼容%d/%s/%b/%o/%n%{private}s脱敏写法,保留原有使用习惯;
    • 无占位符模式:直接传参数,工具类自动匹配类型格式化,仅用%{private}标记实现脱敏;
    • 通用功能:长日志分段(规避4096(1024×4)字节截断)、环境开关、模块TAG筛选。
  3. 实战落地性:工具类可直接复制编译运行,全局替换快速完成日志迁移,契合ArkTS模块化开发理念。

一、工程结构

基于上一节ClassObjectDemo_9复制并重命名为ClassObjectDemo_Complete,核心目录如何下:

  ClassObjectDemo_Complete
  ├─ AppScope
  ├─ entry
  │  ├─ src
  │  │  ├─ main
  │  │  │  ├─ ets
  │  │  │  │  ├─ constants
  │  │  │  │  ├─ entryability
  │  │  │  │  ├─ entrybackupability
  │  │  │  │  ├─ model
  │  │  │  │  ├─ pages
  │  │  │  │  ├─ utils # 通用工具类:新增HiLogUtil
  │  │  │  │  │  ├─ JsonUtil.ets
  │  │  │  │  │  ├─ HiLogUtil.ets
  │  │  │  │  │  └─ ValidationUtil.ets
  │  │  │  │  └─ viewmodel
  │  │  │  ├─ resources
  │  │  │  └─ module.json5
  │  ├─ mock
  │  ├─ ohosTest
  │  └─ test

二、日志工具选型核心:hilog替代console的必要性

在鸿蒙ArkTS应用开发中,console.log的固定参数、无分类、无扩展等短板,无法满足多模块协作与高效调试需求。而hilog作为鸿蒙系统原生日志工具,完全适配ArkTS强类型、可维护的开发理念,是开发阶段与测试阶段的最优选择。

2.1 核心差异对比

特性 console.log hilog
配置灵活性 固定领域标识、TAG、级别,不可自定义 领域标识(0x0000~0xFFFF)、TAG、级别全自定义,适配模块化开发
输出结构 纯文本输出,无分类标识 结构化四元组(领域+TAG+级别+正文),Log面板多维度筛选
敏感数据处理 无脱敏功能,存在数据保护漏洞 支持脱敏配置,可通过占位符/标记实现隐私数据保护
占位符使用 需手动编写%d/%s等,类型匹配繁琐 可通过工具类封装实现双模式兼容(可输/不输占位符)
日志长度限制 存在隐性长度限制,超长日志易被截断 明确4096(1024×4)字节限制,可通过工具类分段处理避免截断
适用场景 开发阶段临时调试 多模块开发、测试排查、日志分类管理

2.2 双模式兼容的核心价值

系统hilog使用需要手动编写占位符,且需保证参数类型与占位符匹配,增加开发成本;而纯无占位符模式又会让习惯原有写法的开发者感到不适。通过对系统hilog封装,既保留了占位符的使用习惯,又提供了极简的参数传递方式,兼顾了不同开发者的使用偏好,同时降低了学习和迁移成本。

三、HiLogUtil工具类

工具类存放路径:entry/src/main/ets/utils/HiLogUtil.ets

3.1 完整代码实现

import { hilog } from '@kit.PerformanceAnalysisKit';

// 日志参数类型声明 (不支持bigint)
export type LogParam = string | number | boolean  | object | undefined | null;

// 结构化返回接口(保留format字段,支持双模式处理)
interface ParseResult {
  tag: string;
  format: string;
  params: LogParam[];
}

/**
 * hilog工具类
 * 核心特性:
 * 1. 双模式兼容:支持占位符模式(%d/%s/%b/%o/%n)和无占位符模式(直接传参);
 * 2. 敏感数据脱敏:占位符模式支持%{private}s,无占位符模式支持%{private}标记;
 * 3. 长日志分段:自动处理4096字节限制,避免日志截断;
 * 4. 环境开关:可屏蔽DEBUG级别日志;
 * 5. 模块筛选:支持自定义TAG,实现模块日志分类。
 */
export class HiLogUtil {
  // 业务域标识(0x0000~0xFFFF,建议按业务模块划分)
  private static readonly DOMAIN: number = 0x0001; 
  // 全局默认日志标签
  private static readonly DEFAULT_TAG: string = 'AppCore';
  // 日志分段阈值(小于4096字节,预留冗余空间) 
  private static readonly MAX_SEGMENT_LEN: number = 3500; 
   // 环境开关(开发=true/测试/生产=false,屏蔽DEBUG日志)
  public static isDebug: boolean = true;
  // 固定格式化字符串(遵循hilog调用规范)
  private static readonly BASE_FORMAT: string = '%{public}s'; 
  // 无占位符模式的脱敏标记
  private static readonly PRIVATE_MARKER: string = '%{private}'; 
  // 占位符正则:匹配%d/%s/%b/%o/%n、%{public}xxx、%{private}xxx(用于检测占位符模式)
  private static readonly PLACEHOLDER_REG = /%({(public|private)})?([dsbon])/g;
  // 私有构造器:禁止实例化工具类(静态工具类设计模式)
  private constructor() {}

  /** DEBUG级别:开发调试专用,测试/生产环境可屏蔽 */
  public static debug(format: string, ...args: LogParam[]): void;
  public static debug(tag: string, format: string, ...args: LogParam[]): void;
  public static debug(...args: LogParam[]): void {
    if (!HiLogUtil.isDebug) return;
    HiLogUtil.processLog(hilog.LogLevel.DEBUG, args);
  }

  /** INFO级别:业务关键节点记录 */
  public static info(format: string, ...args: LogParam[]): void;
  public static info(tag: string, format: string, ...args: LogParam[]): void;
  public static info(...args: LogParam[]): void {
    HiLogUtil.processLog(hilog.LogLevel.INFO, args);
  }

  /** WARN级别:非致命异常警告 */
  public static warn(format: string, ...args: LogParam[]): void;
  public static warn(tag: string, format: string, ...args: LogParam[]): void;
  public static warn(...args: LogParam[]): void {
    HiLogUtil.processLog(hilog.LogLevel.WARN, args);
  }

  /** ERROR级别:功能异常记录 */
  public static error(format: string, ...args: LogParam[]): void;
  public static error(tag: string, format: string, ...args: LogParam[]): void;
  public static error(...args: LogParam[]): void {
    HiLogUtil.processLog(hilog.LogLevel.ERROR, args);
  }

  /** FATAL级别:致命错误记录 */
  public static fatal(format: string, ...args: LogParam[]): void;
  public static fatal(tag: string, format: string, ...args: LogParam[]): void;
  public static fatal(...args: LogParam[]): void {
    HiLogUtil.processLog(hilog.LogLevel.FATAL, args);
  }

  /** 统一处理日志:参数解析→模式判断→格式化/脱敏→分段打印 */
  private static processLog(level: hilog.LogLevel, args: LogParam[]): void {
    const parseResult: ParseResult = HiLogUtil.parseArgs(args);
    const tag: string = parseResult.tag;
    let format: string = parseResult.format;
    let params: LogParam[] = parseResult.params;

    let fullLog: string = '';

    // 步骤1:判断是否为「占位符模式」(格式化字符串包含占位符)
    if (HiLogUtil.hasPlaceholders(format)) {
      // 占位符模式:按原有逻辑处理(补全占位符、格式化参数、脱敏)
      const handledFormat = HiLogUtil.fixPlaceholder(format);
      const handledParams = HiLogUtil.formatParams(params);
      fullLog = HiLogUtil.replacePlaceholder(handledFormat, handledParams);
    } else {
      // 无占位符模式:将format也作为参数,合并后走自动类型格式化+脱敏
      const allParams = [format, ...params];
      fullLog = HiLogUtil.formatAndDesensitize(allParams);
    }

    // 步骤2:分段打印长日志(规避4096字节限制)
    let remainingLog: string = fullLog;
    while (remainingLog.length > 0) {
      const segment: string = remainingLog.slice(0, HiLogUtil.MAX_SEGMENT_LEN);
      remainingLog = remainingLog.slice(HiLogUtil.MAX_SEGMENT_LEN);
      HiLogUtil.callHilog(level, tag, segment);
    }
  }

  /** 遍历解析参数:识别「模块TAG」「格式化字符串」「内容参数」 */
  private static parseArgs(args: LogParam[]): ParseResult {
    const result: ParseResult = {
      tag: HiLogUtil.DEFAULT_TAG,
      format: '',
      params: []
    };

    for (let i = 0; i < args.length; i++) {
      const arg: LogParam = args[i];

      // 1. 识别自定义TAG:第一个参数为字符串,且下一个参数也为字符串
      if (i === 0 && typeof arg === 'string' && args.length > 1) {
        const nextArg: LogParam = args[i + 1];
        if (typeof nextArg === 'string') {
          result.tag = arg as string;
          continue;
        }
      }

      // 2. 识别格式化字符串:第一个非TAG的字符串
      if (result.format.length === 0 && typeof arg === 'string') {
        result.format = arg as string;
        continue;
      }

      // 3. 剩余参数归入内容参数
      result.params.push(arg);
    }

    // 异常处理:无格式化字符串时的默认提示
    if (result.format.length === 0) {
      result.format = '日志参数错误:请传入参数或「TAG+参数」';
    }

    return result;
  }

  /** 辅助方法:检测字符串是否包含占位符 */
  private static hasPlaceholders(str: string): boolean {
    // 重置正则匹配状态,避免多次匹配出现异常(正则对象的lastIndex陷阱)
    HiLogUtil.PLACEHOLDER_REG.lastIndex = 0;
    return HiLogUtil.PLACEHOLDER_REG.test(str);
  }

  /** 补全占位符:将%s/%d等无修饰符的占位符自动转为%{public}s */
  private static fixPlaceholder(format: string): string {
    // 为replace回调参数添加显式类型注解
    return format.replace(/%(?![{]private|[{]public)(d|s|b|o|n)/g, (_: string, placeholder: string) => {
      return `%{public}${placeholder}`;
    });
  }

  /** 格式化参数:对象转JSON,null/undefined转字符串(占位符模式) */
  private static formatParams(params: LogParam[]): string[] {
    const handledParams: string[] = [];
    for (let i = 0; i < params.length; i++) {
      const param: LogParam = params[i];
      if (param === null || param === undefined) {
        handledParams.push(String(param));
      } else if (typeof param === 'object') {
        // 对象/数组转格式化JSON,提升可读性(缩进2个空格)
        handledParams.push(JSON.stringify(param, null, 2));
      } else {
        handledParams.push(String(param));
      }
    }
    return handledParams;
  }

  /** 替换占位符:处理敏感数据脱敏(占位符模式,修复type参数类型注解) */
  private static replacePlaceholder(format: string, params: string[]): string {
    let index: number = 0;
    // 为回调参数添加显式类型注解,处理type为undefined的边界情况
    return format.replace(/%\{(public|private)\}(d|s|b|o|n)/g, (_: string, type?: string) => {
      const param: string = params[index] || '';
      index++;
      // 兜底处理:type为undefined时默认按public处理
      return (type || 'public') === 'private' ? '<private>' : param;
    });
  }

  // ====== 无占位符模式相关方法(自动类型格式化+脱敏)======
  /** 自动格式化参数类型 + 处理敏感数据脱敏(无占位符模式) */
  private static formatAndDesensitize(params: LogParam[]): string {
    let logContent: string = '';
    let needDesensitize: boolean = false; // 标记下一个参数是否需要脱敏

    for (let i = 0; i < params.length; i++) {
      const param: LogParam = params[i];

      // 1. 识别脱敏标记:如果当前参数是%{private},则下一个参数需要脱敏
      if (typeof param === 'string' && param === HiLogUtil.PRIVATE_MARKER) {
        needDesensitize = true;
        continue; // 跳过标记本身,不加入日志内容
      }

      // 2. 自动格式化参数(根据类型处理,替代占位符逻辑)
      let formattedParam: string = HiLogUtil.formatSingleParam(param);

      // 3. 脱敏处理:如果标记为需要脱敏,则替换为<private>
      if (needDesensitize) {
        formattedParam = '<private>';
        needDesensitize = false; // 重置脱敏标记
      }

      // 4. 拼接参数(用空格分隔,提升日志可读性)
      logContent += formattedParam + ' ';
    }

    // 去除末尾多余的空格
    return logContent.trim();
  }

  /** 自动格式化单个参数 */
  private static formatSingleParam(param: LogParam): string {
    switch (typeof param) {
      case 'string':
        return param; // 字符串直接返回
      case 'number':
      // case 'bigint':
      case 'boolean':
        return String(param); // 数字、布尔、大整数转字符串
      case 'object':
        // null单独处理(typeof null === 'object',需特殊判断)
        if (param === null) {
          return 'null';
        }
        // 对象/数组转格式化JSON(缩进2个空格)
        return JSON.stringify(param, null, 2);
      case 'undefined':
        return 'undefined'; // 未定义转字符串
      default:
        return 'unknown'; // 未知类型(兜底处理)
    }
  }

  /** 调用hilog原生接口 */
  private static callHilog(level: hilog.LogLevel, tag: string, content: string): void {
    switch (level) {
      case hilog.LogLevel.DEBUG:
        hilog.debug(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
        break;
      case hilog.LogLevel.INFO:
        hilog.info(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
        break;
      case hilog.LogLevel.WARN:
        hilog.warn(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
        break;
      case hilog.LogLevel.ERROR:
        hilog.error(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
        break;
      case hilog.LogLevel.FATAL:
        hilog.fatal(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
        break;
      default:
        hilog.info(HiLogUtil.DOMAIN, tag, HiLogUtil.BASE_FORMAT, content);
    }
  }
}

四、实战使用

4.1 前置准备:定义测试数据接口与实例

// 定义测试数据接口
interface RequestParams {
  data: string;
  page: number;
  size: number;
}

interface BigDataResponse {
  code: number;
  data: RequestParams[];
}

// 初始化测试数据
const requestParams: RequestParams = { page: 10, size: 20, data: "测试数据内容" };
const bigData: BigDataResponse = {
  code: 200,
  data: Array(100).fill(requestParams) // 生成100条测试数据,模拟超长JSON
};

4.2 模式1:占位符模式

适用于习惯编写%d/%s等占位符的开发者,原有代码无需修改即可直接使用:

// 导入工具类(匹配工程结构的实际路径)
import { HiLogUtil } from '../utils/HiLogUtil';

// 1. 无模块TAG(使用默认TAG=AppCore)
HiLogUtil.info("商品数量:%d,是否上架:%b", 50, true);
HiLogUtil.warn("用户手机号:%{private}s", "13800138000"); // 敏感数据脱敏
HiLogUtil.error("数据解析失败:%s,错误码:%d", "JSON格式错误", 500);

// 2. 带模块TAG(自定义TAG=GoodsModule/UserModule)
HiLogUtil.debug("GoodsModule", "接口请求参数:%o", requestParams); // 对象参数
HiLogUtil.fatal("ServiceModule", "核心服务启动失败:%s", "数据库连接超时");

4.3 模式2:无占位符模式

适用于希望简化开发的开发者,直接传递参数,工具类自动处理类型格式化:

// 导入工具类(匹配工程结构的实际路径)
import { HiLogUtil } from '../utils/HiLogUtil';

// 1. 无模块TAG(使用默认TAG=AppCore)
HiLogUtil.info("商品数量:", 50, "是否上架:", true, "商品信息:", requestParams, null, undefined);

// 2. 带模块TAG(自定义TAG=UserModule/DataModule)
HiLogUtil.debug("UserModule", "用户名称:", "张三", "手机号:", "%{private}", "13800138000"); // 敏感数据脱敏
HiLogUtil.error("DataModule", "数据解析失败:", "JSON格式错误", "错误详情:", new Error("invalid json"));

4.4 通用进阶功能

(1)模块日志筛选

通过自定义模块TAG,可在鸿蒙DevEco Studio的Log面板快速筛选指定模块的日志,提升调试效率:

// 商品模块日志(TAG=GoodsModule)
HiLogUtil.info("GoodsModule", "商品列表加载完成,共", 20, "条数据"); // 无占位符模式
HiLogUtil.info("GoodsModule", "商品库存:%d件", 100); // 占位符模式

// 用户模块日志(TAG=UserModule)
HiLogUtil.info("UserModule", "用户登录成功,ID:", 1001); // 无占位符模式
HiLogUtil.info("UserModule", "用户登录成功,ID:%d", 1001); // 占位符模式

// 在Log面板筛选「TAG=GoodsModule」,仅显示商品模块日志,过滤无关日志

测试打印效果(1)
hilog日志封装

(2)长日志分段(自动处理4096字节限制)

hilog存在4096字节的输出限制,工具类会自动将长日志分段打印,无需手动处理:

// 对比测试:console会截断超长日志,HiLogUtil会分段打印
console.debug("LongLogModule", "使用console接口返回大数据:%o", bigData);

// 占位符模式:自动分段打印
HiLogUtil.debug("LongLogModule", "使用HiLogUtil接口返回大数据:%o", bigData);

// 无占位符模式:自动分段打印
HiLogUtil.debug("LongLogModule", "使用HiLogUtil接口返回大数据:", bigData);

测试打印效果(2)
json大数据换行打印

(3)环境动态开关(区分开发/生产环境)

在项目入口文件中设置环境开关,可屏蔽测试/生产环境的DEBUG日志,仅保留关键日志(INFO/WARN/ERROR/FATAL):

// 项目入口文件(如entryability/index.ets)
import { HiLogUtil } from '../utils/HiLogUtil';

// 测试/生产环境屏蔽DEBUG日志,仅保留INFO及以上级别
// 这里演示,真实开发中需配置自动切换环境。
HiLogUtil.isDebug = true// 开发环境=true,生产环境=false

// DEBUG日志:开发环境显示,生产环境隐藏(无论哪种模式)
HiLogUtil.debug("这行DEBUG日志仅在开发环境显示");
// INFO/WARN/ERROR/FATAL日志:所有环境正常显示
HiLogUtil.info("这行INFO日志正常显示");
HiLogUtil.warn("这行WARN日志正常显示");

测试打印效果(3)
测试不同环境打印日志

五、日志迁移技巧(高效替换console)

5.1 全局替换步骤(DevEco Studio)

  1. 打开全局替换功能:使用快捷键Ctrl+Shift+R(Windows)/Cmd+Shift+R(Mac);
  2. 按以下规则批量替换原有console代码(保留参数,仅替换方法名):
原console代码 替换为HiLogUtil代码 对应日志级别 适用场景
console.log( HiLogUtil.info( INFO(信息) 业务关键节点记录
console.debug( HiLogUtil.debug( DEBUG(调试) 开发阶段调试信息
console.warn( HiLogUtil.warn( WARN(警告) 非致命异常警告
console.error( HiLogUtil.error( ERROR(错误) 功能异常记录

5.2 提升日志实用性

  1. 添加模块TAG:给多模块日志添加专属TAG(如HiLogUtil.info("OrderModule", "订单创建成功")),便于日志筛选;
  2. 敏感数据脱敏:对隐私数据(手机号、密码、身份证号)添加脱敏处理(占位符模式用%{private}s,无占位符模式用%{private}标记);
  3. 对象参数格式化:对对象/数组参数,占位符模式使用%o占位符,无占位符模式直接传递,工具类会自动格式化JSON;
  4. 环境变量自动切换:配置自动切换isDebug状态,避免手动修改代码。

5.3 常见问题与解决方案

问题现象 原因分析 解决方案
日志不显示 1. DOMAIN配置错误;2. TAG筛选错误;3. isDebug=false 1. 检查DOMAIN是否为0x0001(或自定义值);2. 取消TAG筛选或选择正确TAG;3. 开发环境设置isDebug=true
脱敏功能失效 1. 占位符写法错误;2. 标记位置错误 1. 占位符模式使用%{private}s(而非%private%s);2. 无占位符模式将%{private}放在脱敏参数前
长日志仍被截断 MAX_SEGMENT_LEN设置过大 将MAX_SEGMENT_LEN调整为3500~4000之间(建议3500,预留冗余空间)

六、内容总结

本次实现的HiLogUtil工具类为生产级可用版本,核心解决了传统hilog使用的痛点,同时结合了静态方法、函数重载、静态属性、正则表达式等核心知识点,既是日志管理工具,也是0基础开发者的实战训练案例。

核心要点

  1. 双模式兼容:自动识别参数中是否包含占位符,分支处理占位符模式和无占位符模式,开发者可自由选择使用习惯,原有代码无需修改即可迁移。
  2. 功能完整且健壮:支持敏感数据脱敏、长日志分段、模块TAG筛选、环境开关,处理了正则匹配、类型注解、边界值等易出错场景,符合生产级开发规范。
  3. 低学习成本与高效迁移:占位符模式保留原有使用习惯,无占位符模式无需记忆占位符规则;全局替换console的逻辑简单,可快速完成日志体系迁移。
  4. 知识点实战强化:工具类封装过程中综合运用了静态工具类设计模式函数重载正则表达式类型注解等核心知识点,提升开发者的实战能力。

七、仓库代码

八、阶段总结与界面开发阶段预告

基础阶段35节内容已全部圆满收官!回顾这一阶段,我们从ArkTS核心语法(类型、函数、类、接口、正则等)打牢基础,再通过商品管理项目完成了工程化封装的完整实践——不管是代码分层、工具类设计,还是日志体系(HiLogUtil)的标准化落地,都让你从“只会写零散代码”升级为“能按工程化规范开发”,为后续界面开发筑牢了底层能力。

接下来,我们正式进入 ArkUI阶段!这一阶段的核心目标是:从“逻辑层”转向“交互层”,系统掌握鸿蒙ArkTS各类UI组件的使用方法、布局逻辑和交互设计,最终能独立搭建美观、流畅、符合鸿蒙设计规范的应用界面,让之前写的业务逻辑“有界面可承载”。

posted @ 2026-01-25 12:12  鸿蒙-散修  阅读(0)  评论(0)    收藏  举报